django-cfg 1.2.29__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.
Files changed (126) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin/__init__.py +3 -2
  3. django_cfg/apps/payments/admin/balance_admin.py +18 -18
  4. django_cfg/apps/payments/admin/currencies_admin.py +319 -131
  5. django_cfg/apps/payments/admin/payments_admin.py +15 -4
  6. django_cfg/apps/payments/config/module.py +2 -2
  7. django_cfg/apps/payments/config/utils.py +2 -2
  8. django_cfg/apps/payments/decorators.py +2 -2
  9. django_cfg/apps/payments/management/commands/README.md +95 -127
  10. django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
  11. django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
  12. django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
  13. django_cfg/apps/payments/managers/__init__.py +3 -2
  14. django_cfg/apps/payments/managers/balance_manager.py +2 -2
  15. django_cfg/apps/payments/managers/currency_manager.py +272 -49
  16. django_cfg/apps/payments/managers/payment_manager.py +161 -13
  17. django_cfg/apps/payments/middleware/api_access.py +2 -2
  18. django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
  19. django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
  20. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
  21. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
  22. django_cfg/apps/payments/models/__init__.py +3 -2
  23. django_cfg/apps/payments/models/currencies.py +187 -71
  24. django_cfg/apps/payments/models/payments.py +3 -2
  25. django_cfg/apps/payments/serializers/__init__.py +3 -2
  26. django_cfg/apps/payments/serializers/currencies.py +20 -12
  27. django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
  28. django_cfg/apps/payments/services/core/balance_service.py +2 -2
  29. django_cfg/apps/payments/services/core/fallback_service.py +2 -2
  30. django_cfg/apps/payments/services/core/payment_service.py +3 -6
  31. django_cfg/apps/payments/services/core/subscription_service.py +4 -7
  32. django_cfg/apps/payments/services/internal_types.py +171 -7
  33. django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
  34. django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
  35. django_cfg/apps/payments/services/providers/base.py +144 -43
  36. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
  37. django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
  38. django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
  39. django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
  40. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
  41. django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
  42. django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
  43. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
  44. django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
  45. django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
  46. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
  47. django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
  48. django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
  49. django_cfg/apps/payments/services/providers/registry.py +294 -11
  50. django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
  51. django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
  52. django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
  53. django_cfg/apps/payments/services/security/error_handler.py +6 -8
  54. django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
  55. django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
  56. django_cfg/apps/payments/signals/api_key_signals.py +2 -2
  57. django_cfg/apps/payments/signals/payment_signals.py +11 -5
  58. django_cfg/apps/payments/signals/subscription_signals.py +2 -2
  59. django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
  60. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
  61. django_cfg/apps/payments/templates/payments/base.html +4 -4
  62. django_cfg/apps/payments/templates/payments/components/payment_card.html +6 -6
  63. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +4 -4
  64. django_cfg/apps/payments/templates/payments/components/progress_bar.html +14 -7
  65. django_cfg/apps/payments/templates/payments/components/provider_stats.html +2 -2
  66. django_cfg/apps/payments/templates/payments/components/status_badge.html +8 -1
  67. django_cfg/apps/payments/templates/payments/components/status_overview.html +34 -30
  68. django_cfg/apps/payments/templates/payments/dashboard.html +202 -290
  69. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
  70. django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
  71. django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
  72. django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
  73. django_cfg/apps/payments/templates/payments/stats.html +261 -0
  74. django_cfg/apps/payments/templates/payments/test.html +213 -0
  75. django_cfg/apps/payments/urls.py +3 -1
  76. django_cfg/apps/payments/{urls_templates.py → urls_admin.py} +6 -0
  77. django_cfg/apps/payments/utils/__init__.py +1 -3
  78. django_cfg/apps/payments/utils/billing_utils.py +2 -2
  79. django_cfg/apps/payments/utils/config_utils.py +2 -8
  80. django_cfg/apps/payments/utils/validation_utils.py +2 -2
  81. django_cfg/apps/payments/views/__init__.py +3 -2
  82. django_cfg/apps/payments/views/currency_views.py +31 -20
  83. django_cfg/apps/payments/views/payment_views.py +2 -2
  84. django_cfg/apps/payments/views/templates/ajax.py +141 -2
  85. django_cfg/apps/payments/views/templates/base.py +21 -13
  86. django_cfg/apps/payments/views/templates/payment_detail.py +1 -1
  87. django_cfg/apps/payments/views/templates/payment_management.py +34 -40
  88. django_cfg/apps/payments/views/templates/stats.py +8 -4
  89. django_cfg/apps/payments/views/webhook_views.py +2 -2
  90. django_cfg/apps/payments/viewsets.py +3 -2
  91. django_cfg/apps/tasks/urls.py +0 -2
  92. django_cfg/apps/tasks/urls_admin.py +14 -0
  93. django_cfg/apps/urls.py +4 -4
  94. django_cfg/core/config.py +35 -0
  95. django_cfg/models/payments.py +2 -8
  96. django_cfg/modules/django_currency/__init__.py +16 -11
  97. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  98. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  99. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  100. django_cfg/modules/django_currency/core/__init__.py +1 -7
  101. django_cfg/modules/django_currency/core/converter.py +18 -23
  102. django_cfg/modules/django_currency/core/models.py +122 -11
  103. django_cfg/modules/django_currency/database/__init__.py +4 -4
  104. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  105. django_cfg/modules/django_unfold/dashboard.py +7 -2
  106. django_cfg/template_archive/django_sample.zip +0 -0
  107. django_cfg/templates/admin/components/action_grid.html +9 -9
  108. django_cfg/templates/admin/components/metric_card.html +5 -5
  109. django_cfg/templates/admin/components/status_badge.html +2 -2
  110. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  111. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  112. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  113. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  114. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/METADATA +2 -4
  115. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/RECORD +118 -96
  116. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  117. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  118. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  119. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  120. django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
  121. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  122. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  123. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  124. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
  125. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
  126. {django_cfg-1.2.29.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
+ ]