django-cfg 1.2.27__py3-none-any.whl → 1.2.31__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/payments/admin/__init__.py +3 -2
- django_cfg/apps/payments/admin/balance_admin.py +18 -18
- django_cfg/apps/payments/admin/currencies_admin.py +319 -131
- django_cfg/apps/payments/admin/payments_admin.py +15 -4
- django_cfg/apps/payments/config/module.py +2 -2
- django_cfg/apps/payments/config/utils.py +2 -2
- django_cfg/apps/payments/decorators.py +2 -2
- django_cfg/apps/payments/management/commands/README.md +95 -127
- django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
- django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
- django_cfg/apps/payments/managers/__init__.py +3 -2
- django_cfg/apps/payments/managers/balance_manager.py +2 -2
- django_cfg/apps/payments/managers/currency_manager.py +272 -49
- django_cfg/apps/payments/managers/payment_manager.py +161 -13
- django_cfg/apps/payments/middleware/api_access.py +2 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
- django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
- django_cfg/apps/payments/models/__init__.py +3 -2
- django_cfg/apps/payments/models/currencies.py +187 -71
- django_cfg/apps/payments/models/payments.py +3 -2
- django_cfg/apps/payments/serializers/__init__.py +3 -2
- django_cfg/apps/payments/serializers/currencies.py +20 -12
- django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
- django_cfg/apps/payments/services/core/balance_service.py +2 -2
- django_cfg/apps/payments/services/core/fallback_service.py +2 -2
- django_cfg/apps/payments/services/core/payment_service.py +3 -6
- django_cfg/apps/payments/services/core/subscription_service.py +4 -7
- django_cfg/apps/payments/services/internal_types.py +171 -7
- django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
- django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
- django_cfg/apps/payments/services/providers/base.py +144 -43
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
- django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
- django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
- django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
- django_cfg/apps/payments/services/providers/registry.py +294 -11
- django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
- django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
- django_cfg/apps/payments/services/security/error_handler.py +6 -8
- django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
- django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/payment_signals.py +11 -5
- django_cfg/apps/payments/signals/subscription_signals.py +2 -2
- django_cfg/apps/payments/static/payments/css/payments.css +340 -0
- django_cfg/apps/payments/static/payments/js/notifications.js +202 -0
- django_cfg/apps/payments/static/payments/js/payment-utils.js +318 -0
- django_cfg/apps/payments/static/payments/js/theme.js +86 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
- django_cfg/apps/payments/templates/payments/base.html +182 -0
- django_cfg/apps/payments/templates/payments/components/payment_card.html +201 -0
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +109 -0
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +43 -0
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +40 -0
- django_cfg/apps/payments/templates/payments/components/status_badge.html +34 -0
- django_cfg/apps/payments/templates/payments/components/status_overview.html +148 -0
- django_cfg/apps/payments/templates/payments/dashboard.html +258 -0
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
- django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
- django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
- django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
- django_cfg/apps/payments/templates/payments/stats.html +261 -0
- django_cfg/apps/payments/templates/payments/test.html +213 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -0
- django_cfg/apps/payments/templatetags/payments_tags.py +315 -0
- django_cfg/apps/payments/urls.py +3 -1
- django_cfg/apps/payments/urls_admin.py +58 -0
- django_cfg/apps/payments/utils/__init__.py +1 -3
- django_cfg/apps/payments/utils/billing_utils.py +2 -2
- django_cfg/apps/payments/utils/config_utils.py +2 -8
- django_cfg/apps/payments/utils/validation_utils.py +2 -2
- django_cfg/apps/payments/views/__init__.py +3 -2
- django_cfg/apps/payments/views/currency_views.py +31 -20
- django_cfg/apps/payments/views/payment_views.py +2 -2
- django_cfg/apps/payments/views/templates/__init__.py +25 -0
- django_cfg/apps/payments/views/templates/ajax.py +451 -0
- django_cfg/apps/payments/views/templates/base.py +212 -0
- django_cfg/apps/payments/views/templates/dashboard.py +60 -0
- django_cfg/apps/payments/views/templates/payment_detail.py +102 -0
- django_cfg/apps/payments/views/templates/payment_management.py +158 -0
- django_cfg/apps/payments/views/templates/qr_code.py +174 -0
- django_cfg/apps/payments/views/templates/stats.py +244 -0
- django_cfg/apps/payments/views/templates/utils.py +181 -0
- django_cfg/apps/payments/views/webhook_views.py +2 -2
- django_cfg/apps/payments/viewsets.py +3 -2
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +6 -3
- django_cfg/core/config.py +35 -0
- django_cfg/models/payments.py +2 -8
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_unfold/dashboard.py +7 -2
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/.gitignore +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/METADATA +13 -18
- {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/RECORD +130 -83
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -310
- django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,564 @@
|
|
1
|
+
"""
|
2
|
+
Cryptomus payment provider implementation using official library.
|
3
|
+
Enhanced with better error handling, security, and Pydantic models.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from decimal import Decimal
|
7
|
+
from typing import Dict, Any, Optional, List
|
8
|
+
from datetime import datetime
|
9
|
+
|
10
|
+
from cryptomus import Client
|
11
|
+
from cryptomus.request_exceptions import RequestExceptionsBuilder
|
12
|
+
|
13
|
+
from ..base import PaymentProvider
|
14
|
+
from ...internal_types import ProviderResponse, WebhookData, PaymentAmountEstimate, ProviderInfo
|
15
|
+
from .models import CryptomusConfig, CryptomusWebhook
|
16
|
+
from django_cfg.modules.django_logger import get_logger
|
17
|
+
|
18
|
+
logger = get_logger("cryptomus")
|
19
|
+
|
20
|
+
|
21
|
+
class CryptomusProviderV2(PaymentProvider):
|
22
|
+
"""
|
23
|
+
Enhanced Cryptomus payment provider using official Python library.
|
24
|
+
Features:
|
25
|
+
- Official cryptomus library integration
|
26
|
+
- Proper error handling with RequestExceptionsBuilder
|
27
|
+
- Pydantic model validation
|
28
|
+
- Universal field mapping
|
29
|
+
- Security-first approach
|
30
|
+
"""
|
31
|
+
|
32
|
+
name = "cryptomus_v2"
|
33
|
+
display_name = "Cryptomus (Official)"
|
34
|
+
|
35
|
+
def __init__(self, config: CryptomusConfig):
|
36
|
+
super().__init__(config)
|
37
|
+
self.config = config
|
38
|
+
self._payment_client = None
|
39
|
+
self._payout_client = None
|
40
|
+
|
41
|
+
@property
|
42
|
+
def payment_client(self):
|
43
|
+
"""Lazy-loaded payment client using official library."""
|
44
|
+
if self._payment_client is None:
|
45
|
+
try:
|
46
|
+
self._payment_client = Client.payment(
|
47
|
+
self.config.api_key,
|
48
|
+
self.config.merchant_id
|
49
|
+
)
|
50
|
+
except Exception as e:
|
51
|
+
logger.error(f"Failed to initialize Cryptomus payment client: {e}")
|
52
|
+
raise
|
53
|
+
return self._payment_client
|
54
|
+
|
55
|
+
@property
|
56
|
+
def payout_client(self):
|
57
|
+
"""Lazy-loaded payout client using official library."""
|
58
|
+
if self._payout_client is None and hasattr(self.config, 'payout_api_key'):
|
59
|
+
try:
|
60
|
+
self._payout_client = Client.payout(
|
61
|
+
self.config.payout_api_key,
|
62
|
+
self.config.merchant_id
|
63
|
+
)
|
64
|
+
except Exception as e:
|
65
|
+
logger.error(f"Failed to initialize Cryptomus payout client: {e}")
|
66
|
+
# Don't raise here, payout is optional
|
67
|
+
pass
|
68
|
+
return self._payout_client
|
69
|
+
|
70
|
+
def create_payment(self, payment_data: dict) -> ProviderResponse:
|
71
|
+
"""
|
72
|
+
Create payment using official Cryptomus library.
|
73
|
+
Maps to universal payment fields with enhanced error handling.
|
74
|
+
"""
|
75
|
+
try:
|
76
|
+
# Validate required fields
|
77
|
+
order_id = payment_data.get('order_id')
|
78
|
+
amount = payment_data.get('amount')
|
79
|
+
currency = payment_data.get('currency', 'USD')
|
80
|
+
|
81
|
+
if not all([order_id, amount]):
|
82
|
+
return ProviderResponse(
|
83
|
+
success=False,
|
84
|
+
error_message="Missing required fields: order_id, amount"
|
85
|
+
)
|
86
|
+
|
87
|
+
# Validate amount
|
88
|
+
try:
|
89
|
+
amount_decimal = Decimal(str(amount))
|
90
|
+
if amount_decimal <= 0:
|
91
|
+
return ProviderResponse(
|
92
|
+
success=False,
|
93
|
+
error_message="Amount must be positive"
|
94
|
+
)
|
95
|
+
except (ValueError, TypeError):
|
96
|
+
return ProviderResponse(
|
97
|
+
success=False,
|
98
|
+
error_message="Invalid amount format"
|
99
|
+
)
|
100
|
+
|
101
|
+
# Prepare payment data for Cryptomus API
|
102
|
+
cryptomus_data = {
|
103
|
+
'amount': str(amount_decimal),
|
104
|
+
'currency': currency,
|
105
|
+
'order_id': order_id,
|
106
|
+
'url_callback': payment_data.get('callback_url', self.config.callback_url),
|
107
|
+
'url_return': payment_data.get('return_url'),
|
108
|
+
'url_success': payment_data.get('success_url', self.config.success_url),
|
109
|
+
'url_cancel': payment_data.get('cancel_url', self.config.cancel_url),
|
110
|
+
'is_payment_multiple': payment_data.get('allow_overpayment', True),
|
111
|
+
'lifetime': payment_data.get('lifetime', 3600), # 1 hour default
|
112
|
+
'to_currency': payment_data.get('crypto_currency', 'BTC')
|
113
|
+
}
|
114
|
+
|
115
|
+
# Add optional fields
|
116
|
+
if payment_data.get('description'):
|
117
|
+
cryptomus_data['additional_data'] = {
|
118
|
+
'description': payment_data['description']
|
119
|
+
}
|
120
|
+
|
121
|
+
# Create payment using official library
|
122
|
+
result = self.payment_client.create(cryptomus_data)
|
123
|
+
|
124
|
+
# Map response to universal format
|
125
|
+
return ProviderResponse(
|
126
|
+
success=True,
|
127
|
+
transaction_id=result.get('uuid'),
|
128
|
+
provider_payment_id=result.get('uuid'),
|
129
|
+
pay_address=result.get('address'),
|
130
|
+
pay_amount=Decimal(str(result.get('payment_amount', 0))),
|
131
|
+
pay_currency=result.get('payer_currency', currency),
|
132
|
+
data={
|
133
|
+
# Universal fields
|
134
|
+
'payment_url': result.get('url'),
|
135
|
+
'qr_code_data': result.get('url'), # Use payment URL for QR
|
136
|
+
'expires_at': result.get('expired_at'),
|
137
|
+
|
138
|
+
# Cryptomus specific
|
139
|
+
'cryptomus_uuid': result.get('uuid'),
|
140
|
+
'cryptomus_order_id': result.get('order_id'),
|
141
|
+
'cryptomus_status': result.get('payment_status'),
|
142
|
+
'cryptomus_amount': result.get('amount'),
|
143
|
+
'cryptomus_currency': result.get('currency'),
|
144
|
+
'cryptomus_network': result.get('network'),
|
145
|
+
'cryptomus_address': result.get('address'),
|
146
|
+
'cryptomus_is_final': result.get('is_final', False)
|
147
|
+
}
|
148
|
+
)
|
149
|
+
|
150
|
+
except RequestExceptionsBuilder as e:
|
151
|
+
# Handle Cryptomus API exceptions
|
152
|
+
error_msg = f"Cryptomus API error: {e}"
|
153
|
+
|
154
|
+
# Check for specific error types
|
155
|
+
if hasattr(e, 'args') and len(e.args) >= 2:
|
156
|
+
status_code = e.args[1] if isinstance(e.args[1], int) else 0
|
157
|
+
|
158
|
+
if status_code == 400:
|
159
|
+
if 'balance' in str(e).lower():
|
160
|
+
error_msg = "Insufficient balance in merchant account"
|
161
|
+
elif 'amount' in str(e).lower():
|
162
|
+
error_msg = "Invalid payment amount"
|
163
|
+
elif 'currency' in str(e).lower():
|
164
|
+
error_msg = "Unsupported currency"
|
165
|
+
elif status_code == 401:
|
166
|
+
error_msg = "Authentication failed: Invalid API key or merchant ID"
|
167
|
+
elif status_code == 403:
|
168
|
+
error_msg = "Access denied: Check merchant permissions"
|
169
|
+
elif status_code == 422:
|
170
|
+
error_msg = "Validation error: Check request parameters"
|
171
|
+
|
172
|
+
logger.error(f"Cryptomus create_payment error: {error_msg}")
|
173
|
+
return ProviderResponse(
|
174
|
+
success=False,
|
175
|
+
error_message=error_msg
|
176
|
+
)
|
177
|
+
|
178
|
+
except Exception as e:
|
179
|
+
logger.error(f"Unexpected error in Cryptomus create_payment: {e}")
|
180
|
+
return ProviderResponse(
|
181
|
+
success=False,
|
182
|
+
error_message=f"Unexpected error: {str(e)}"
|
183
|
+
)
|
184
|
+
|
185
|
+
def validate_webhook(self, webhook_data: Dict[str, Any],
|
186
|
+
request_headers: Optional[Dict[str, str]] = None,
|
187
|
+
raw_body: Optional[bytes] = None) -> bool:
|
188
|
+
"""
|
189
|
+
Validate Cryptomus webhook using Pydantic model and signature verification.
|
190
|
+
Enhanced security with strict validation.
|
191
|
+
"""
|
192
|
+
try:
|
193
|
+
# Validate using Pydantic model
|
194
|
+
webhook = CryptomusWebhook(**webhook_data)
|
195
|
+
|
196
|
+
# Verify signature if provided
|
197
|
+
signature = webhook.sign
|
198
|
+
if signature:
|
199
|
+
# Prepare data for signature verification (exclude sign field)
|
200
|
+
data_for_sign = webhook.model_dump(exclude={'sign'})
|
201
|
+
expected_signature = self._generate_webhook_signature(data_for_sign)
|
202
|
+
|
203
|
+
if signature != expected_signature:
|
204
|
+
logger.error("Cryptomus webhook signature verification failed")
|
205
|
+
return False
|
206
|
+
else:
|
207
|
+
logger.warning("Cryptomus webhook missing signature - validation skipped")
|
208
|
+
|
209
|
+
# Additional business logic validation
|
210
|
+
if webhook.type not in ['payment', 'payout']:
|
211
|
+
logger.warning(f"Unknown webhook type: {webhook.type}")
|
212
|
+
return False
|
213
|
+
|
214
|
+
return True
|
215
|
+
|
216
|
+
except Exception as e:
|
217
|
+
logger.error(f"Cryptomus webhook validation failed: {e}")
|
218
|
+
return False
|
219
|
+
|
220
|
+
def process_webhook(self, webhook_data: Dict[str, Any]) -> WebhookData:
|
221
|
+
"""
|
222
|
+
Process Cryptomus webhook with enhanced error handling.
|
223
|
+
Maps to universal webhook data format.
|
224
|
+
"""
|
225
|
+
try:
|
226
|
+
# Validate and parse webhook data
|
227
|
+
webhook = CryptomusWebhook(**webhook_data)
|
228
|
+
|
229
|
+
# Map status to universal format
|
230
|
+
universal_status = self._map_status(webhook.status)
|
231
|
+
|
232
|
+
# Calculate amounts
|
233
|
+
pay_amount = webhook.amount if webhook.amount else Decimal('0')
|
234
|
+
actually_paid = webhook.payment_amount if webhook.payment_amount else pay_amount
|
235
|
+
|
236
|
+
return WebhookData(
|
237
|
+
provider_payment_id=webhook.uuid,
|
238
|
+
status=universal_status,
|
239
|
+
pay_amount=pay_amount,
|
240
|
+
pay_currency=webhook.payer_currency or webhook.currency,
|
241
|
+
actually_paid=actually_paid,
|
242
|
+
order_id=webhook.order_id,
|
243
|
+
signature=webhook.txid or webhook.sign,
|
244
|
+
transaction_hash=webhook.txid,
|
245
|
+
network=webhook.network,
|
246
|
+
from_address=webhook.from_address,
|
247
|
+
is_final=webhook.is_final,
|
248
|
+
raw_data=webhook_data
|
249
|
+
)
|
250
|
+
|
251
|
+
except Exception as e:
|
252
|
+
logger.error(f"Cryptomus webhook processing failed: {e}")
|
253
|
+
raise
|
254
|
+
|
255
|
+
def check_payment_status(self, payment_id: str) -> ProviderResponse:
|
256
|
+
"""Get payment status using official library."""
|
257
|
+
try:
|
258
|
+
# Use info method with UUID
|
259
|
+
result = self.payment_client.info({'uuid': payment_id})
|
260
|
+
|
261
|
+
return ProviderResponse(
|
262
|
+
success=True,
|
263
|
+
provider_payment_id=result.get('uuid'),
|
264
|
+
status=self._map_status(result.get('payment_status')),
|
265
|
+
pay_address=result.get('address'),
|
266
|
+
pay_amount=Decimal(str(result.get('payment_amount', 0))),
|
267
|
+
pay_currency=result.get('payer_currency'),
|
268
|
+
data={
|
269
|
+
'transaction_hash': result.get('txid'),
|
270
|
+
'network': result.get('network'),
|
271
|
+
'confirmations': result.get('confirmations', 0),
|
272
|
+
'is_final': result.get('is_final', False),
|
273
|
+
'created_at': result.get('created_at'),
|
274
|
+
'updated_at': result.get('updated_at')
|
275
|
+
}
|
276
|
+
)
|
277
|
+
|
278
|
+
except RequestExceptionsBuilder as e:
|
279
|
+
error_msg = f"Cryptomus payment status error: {e}"
|
280
|
+
logger.error(error_msg)
|
281
|
+
return ProviderResponse(
|
282
|
+
success=False,
|
283
|
+
error_message=error_msg
|
284
|
+
)
|
285
|
+
except Exception as e:
|
286
|
+
logger.error(f"Unexpected error in get_payment_status: {e}")
|
287
|
+
return ProviderResponse(
|
288
|
+
success=False,
|
289
|
+
error_message=f"Unexpected error: {str(e)}"
|
290
|
+
)
|
291
|
+
|
292
|
+
def get_supported_currencies(self) -> ProviderResponse:
|
293
|
+
"""Get supported currencies using official library services endpoint."""
|
294
|
+
try:
|
295
|
+
services = self.payment_client.services()
|
296
|
+
|
297
|
+
# Extract unique currencies
|
298
|
+
currencies = list(set([
|
299
|
+
service.get('currency') for service in services
|
300
|
+
if service.get('currency')
|
301
|
+
]))
|
302
|
+
|
303
|
+
return ProviderResponse(
|
304
|
+
success=True,
|
305
|
+
data={
|
306
|
+
'currencies': currencies,
|
307
|
+
'services': services # Full service data for advanced usage
|
308
|
+
}
|
309
|
+
)
|
310
|
+
|
311
|
+
except RequestExceptionsBuilder as e:
|
312
|
+
error_msg = f"Cryptomus services error: {e}"
|
313
|
+
logger.error(error_msg)
|
314
|
+
return ProviderResponse(
|
315
|
+
success=False,
|
316
|
+
error_message=error_msg
|
317
|
+
)
|
318
|
+
except Exception as e:
|
319
|
+
logger.error(f"Unexpected error in get_supported_currencies: {e}")
|
320
|
+
return ProviderResponse(
|
321
|
+
success=False,
|
322
|
+
error_message=f"Unexpected error: {str(e)}"
|
323
|
+
)
|
324
|
+
|
325
|
+
def get_supported_networks(self, currency_code: str = None) -> ProviderResponse:
|
326
|
+
"""Get supported networks from services data."""
|
327
|
+
try:
|
328
|
+
services_response = self.get_supported_currencies()
|
329
|
+
if not services_response.success:
|
330
|
+
return services_response
|
331
|
+
|
332
|
+
services = services_response.data.get('services', [])
|
333
|
+
networks = {}
|
334
|
+
|
335
|
+
for service in services:
|
336
|
+
currency = service.get('currency', '').upper()
|
337
|
+
network = service.get('network')
|
338
|
+
|
339
|
+
if currency_code and currency != currency_code.upper():
|
340
|
+
continue
|
341
|
+
|
342
|
+
if currency and network:
|
343
|
+
if currency not in networks:
|
344
|
+
networks[currency] = []
|
345
|
+
|
346
|
+
network_info = {
|
347
|
+
'code': network,
|
348
|
+
'name': service.get('network_name', network),
|
349
|
+
'min_amount': service.get('min_amount', 0),
|
350
|
+
'max_amount': service.get('max_amount', 0),
|
351
|
+
'commission_percent': service.get('commission_percent', 0),
|
352
|
+
'is_available': service.get('is_available', True)
|
353
|
+
}
|
354
|
+
networks[currency].append(network_info)
|
355
|
+
|
356
|
+
return ProviderResponse(
|
357
|
+
success=True,
|
358
|
+
data={'networks': networks}
|
359
|
+
)
|
360
|
+
|
361
|
+
except Exception as e:
|
362
|
+
logger.error(f"Error in get_supported_networks: {e}")
|
363
|
+
return ProviderResponse(
|
364
|
+
success=False,
|
365
|
+
error_message=f"Error: {str(e)}"
|
366
|
+
)
|
367
|
+
|
368
|
+
def get_balance(self) -> ProviderResponse:
|
369
|
+
"""Get merchant balance using official library."""
|
370
|
+
try:
|
371
|
+
balance = self.payment_client.balance()
|
372
|
+
|
373
|
+
return ProviderResponse(
|
374
|
+
success=True,
|
375
|
+
data={'balance': balance}
|
376
|
+
)
|
377
|
+
|
378
|
+
except RequestExceptionsBuilder as e:
|
379
|
+
error_msg = f"Cryptomus balance error: {e}"
|
380
|
+
logger.error(error_msg)
|
381
|
+
return ProviderResponse(
|
382
|
+
success=False,
|
383
|
+
error_message=error_msg
|
384
|
+
)
|
385
|
+
except Exception as e:
|
386
|
+
logger.error(f"Unexpected error in get_balance: {e}")
|
387
|
+
return ProviderResponse(
|
388
|
+
success=False,
|
389
|
+
error_message=f"Unexpected error: {str(e)}"
|
390
|
+
)
|
391
|
+
|
392
|
+
def estimate_payment_amount(self, amount: Decimal, currency_code: str) -> Optional[PaymentAmountEstimate]:
|
393
|
+
"""
|
394
|
+
Estimate payment amount by creating a test payment.
|
395
|
+
Note: Cryptomus doesn't have a dedicated estimation API.
|
396
|
+
"""
|
397
|
+
try:
|
398
|
+
# For estimation, we could use the exchange rate or create a test payment
|
399
|
+
# Since there's no dedicated estimation API, we'll return a basic estimate
|
400
|
+
# based on 1:1 ratio and let the actual payment creation determine the real rate
|
401
|
+
|
402
|
+
return PaymentAmountEstimate(
|
403
|
+
currency_from='usd',
|
404
|
+
currency_to=currency_code.lower(),
|
405
|
+
amount_from=amount,
|
406
|
+
estimated_amount=amount, # 1:1 estimate, real rate determined at payment creation
|
407
|
+
exchange_rate=Decimal('1.0'),
|
408
|
+
provider_name=self.name,
|
409
|
+
estimated_at=datetime.now()
|
410
|
+
)
|
411
|
+
|
412
|
+
except Exception as e:
|
413
|
+
logger.error(f"Cryptomus estimate_payment_amount error: {e}")
|
414
|
+
return None
|
415
|
+
|
416
|
+
def create_wallet(self, currency: str, network: str, order_id: str,
|
417
|
+
callback_url: str = None) -> ProviderResponse:
|
418
|
+
"""Create static wallet using official library."""
|
419
|
+
try:
|
420
|
+
wallet_data = {
|
421
|
+
'network': network,
|
422
|
+
'currency': currency,
|
423
|
+
'order_id': order_id
|
424
|
+
}
|
425
|
+
|
426
|
+
if callback_url:
|
427
|
+
wallet_data['url_callback'] = callback_url
|
428
|
+
elif self.config.callback_url:
|
429
|
+
wallet_data['url_callback'] = self.config.callback_url
|
430
|
+
|
431
|
+
result = self.payment_client.create_wallet(wallet_data)
|
432
|
+
|
433
|
+
return ProviderResponse(
|
434
|
+
success=True,
|
435
|
+
provider_payment_id=result.get('uuid'),
|
436
|
+
pay_address=result.get('address'),
|
437
|
+
data={
|
438
|
+
'wallet_uuid': result.get('uuid'),
|
439
|
+
'address': result.get('address'),
|
440
|
+
'currency': currency,
|
441
|
+
'network': network,
|
442
|
+
'order_id': order_id
|
443
|
+
}
|
444
|
+
)
|
445
|
+
|
446
|
+
except RequestExceptionsBuilder as e:
|
447
|
+
error_msg = f"Cryptomus wallet creation error: {e}"
|
448
|
+
logger.error(error_msg)
|
449
|
+
return ProviderResponse(
|
450
|
+
success=False,
|
451
|
+
error_message=error_msg
|
452
|
+
)
|
453
|
+
except Exception as e:
|
454
|
+
logger.error(f"Unexpected error in create_wallet: {e}")
|
455
|
+
return ProviderResponse(
|
456
|
+
success=False,
|
457
|
+
error_message=f"Unexpected error: {str(e)}"
|
458
|
+
)
|
459
|
+
|
460
|
+
def resend_notification(self, payment_id: str = None, order_id: str = None) -> ProviderResponse:
|
461
|
+
"""Resend webhook notification using official library."""
|
462
|
+
try:
|
463
|
+
if not payment_id and not order_id:
|
464
|
+
return ProviderResponse(
|
465
|
+
success=False,
|
466
|
+
error_message="Either payment_id (uuid) or order_id must be provided"
|
467
|
+
)
|
468
|
+
|
469
|
+
data = {}
|
470
|
+
if payment_id:
|
471
|
+
data['uuid'] = payment_id
|
472
|
+
if order_id:
|
473
|
+
data['order_id'] = order_id
|
474
|
+
|
475
|
+
result = self.payment_client.resend_notification(data)
|
476
|
+
|
477
|
+
return ProviderResponse(
|
478
|
+
success=True,
|
479
|
+
data={'resend_result': result}
|
480
|
+
)
|
481
|
+
|
482
|
+
except RequestExceptionsBuilder as e:
|
483
|
+
error_msg = f"Cryptomus resend notification error: {e}"
|
484
|
+
logger.error(error_msg)
|
485
|
+
return ProviderResponse(
|
486
|
+
success=False,
|
487
|
+
error_message=error_msg
|
488
|
+
)
|
489
|
+
except Exception as e:
|
490
|
+
logger.error(f"Unexpected error in resend_notification: {e}")
|
491
|
+
return ProviderResponse(
|
492
|
+
success=False,
|
493
|
+
error_message=f"Unexpected error: {str(e)}"
|
494
|
+
)
|
495
|
+
|
496
|
+
def _generate_webhook_signature(self, webhook_data: dict) -> str:
|
497
|
+
"""Generate expected webhook signature for validation."""
|
498
|
+
import json
|
499
|
+
import base64
|
500
|
+
import hashlib
|
501
|
+
|
502
|
+
# Cryptomus webhook signature: md5(base64(json_data) + api_key)
|
503
|
+
json_data = json.dumps(webhook_data, separators=(',', ':'), sort_keys=True)
|
504
|
+
encoded_data = base64.b64encode(json_data.encode('utf-8')).decode('utf-8')
|
505
|
+
signature_string = encoded_data + self.config.api_key
|
506
|
+
return hashlib.md5(signature_string.encode('utf-8')).hexdigest()
|
507
|
+
|
508
|
+
def _map_status(self, cryptomus_status: str) -> str:
|
509
|
+
"""Map Cryptomus status to universal status."""
|
510
|
+
status_mapping = {
|
511
|
+
'check': 'pending',
|
512
|
+
'process': 'pending',
|
513
|
+
'confirm_check': 'pending',
|
514
|
+
'paid': 'completed',
|
515
|
+
'paid_over': 'completed',
|
516
|
+
'confirmed': 'completed',
|
517
|
+
'fail': 'failed',
|
518
|
+
'wrong_amount': 'failed',
|
519
|
+
'cancel': 'cancelled',
|
520
|
+
'system_fail': 'failed',
|
521
|
+
'refund_process': 'refunding',
|
522
|
+
'refund_fail': 'failed',
|
523
|
+
'refund_paid': 'refunded'
|
524
|
+
}
|
525
|
+
return status_mapping.get(cryptomus_status, 'pending')
|
526
|
+
|
527
|
+
def get_provider_info(self) -> ProviderInfo:
|
528
|
+
"""Get provider information with Pydantic model."""
|
529
|
+
return ProviderInfo(
|
530
|
+
name=self.name,
|
531
|
+
display_name=self.display_name,
|
532
|
+
supported_currencies=["BTC", "ETH", "USDT", "USDC", "LTC", "BCH", "TRX", "BNB"],
|
533
|
+
is_active=self.config.enabled,
|
534
|
+
features={
|
535
|
+
"hosted_payments": True,
|
536
|
+
"static_wallets": True,
|
537
|
+
"payouts": bool(self.payout_client),
|
538
|
+
"webhooks": True,
|
539
|
+
"qr_codes": True,
|
540
|
+
"overpayments": True,
|
541
|
+
"refunds": True,
|
542
|
+
"api_endpoints": {
|
543
|
+
"create_payment": "https://api.cryptomus.com/v1/payment",
|
544
|
+
"payment_info": "https://api.cryptomus.com/v1/payment/info",
|
545
|
+
"services": "https://api.cryptomus.com/v1/payment/services",
|
546
|
+
"balance": "https://api.cryptomus.com/v1/balance",
|
547
|
+
"create_wallet": "https://api.cryptomus.com/v1/wallet"
|
548
|
+
}
|
549
|
+
}
|
550
|
+
)
|
551
|
+
|
552
|
+
def get_currency_network_mapping(self) -> Dict[str, List[str]]:
|
553
|
+
"""Get currency to network mapping."""
|
554
|
+
networks_response = self.get_supported_networks()
|
555
|
+
if not networks_response.success:
|
556
|
+
return {}
|
557
|
+
|
558
|
+
mapping = {}
|
559
|
+
networks_data = networks_response.data.get('networks', {})
|
560
|
+
|
561
|
+
for currency_code, networks in networks_data.items():
|
562
|
+
mapping[currency_code] = [network['code'] for network in networks]
|
563
|
+
|
564
|
+
return mapping
|
@@ -0,0 +1,34 @@
|
|
1
|
+
"""
|
2
|
+
Provider-specific models package.
|
3
|
+
|
4
|
+
Re-exports provider-specific currency and network models.
|
5
|
+
Universal service models are in internal_types.py.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .currencies import (
|
9
|
+
# Enums
|
10
|
+
CurrencyType,
|
11
|
+
NetworkType,
|
12
|
+
|
13
|
+
# Provider-specific currency models
|
14
|
+
CurrencyInfo,
|
15
|
+
NetworkInfo,
|
16
|
+
ProviderCurrencyResponse,
|
17
|
+
ProviderNetworkResponse,
|
18
|
+
CurrencyNetworkMapping,
|
19
|
+
)
|
20
|
+
|
21
|
+
# Monitoring models moved to services/monitoring/api_schemas.py
|
22
|
+
|
23
|
+
__all__ = [
|
24
|
+
# Enums
|
25
|
+
'CurrencyType',
|
26
|
+
'NetworkType',
|
27
|
+
|
28
|
+
# Provider currency models
|
29
|
+
'CurrencyInfo',
|
30
|
+
'NetworkInfo',
|
31
|
+
'ProviderCurrencyResponse',
|
32
|
+
'ProviderNetworkResponse',
|
33
|
+
'CurrencyNetworkMapping',
|
34
|
+
]
|