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.
Files changed (138) 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/static/payments/css/payments.css +340 -0
  60. django_cfg/apps/payments/static/payments/js/notifications.js +202 -0
  61. django_cfg/apps/payments/static/payments/js/payment-utils.js +318 -0
  62. django_cfg/apps/payments/static/payments/js/theme.js +86 -0
  63. django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
  64. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
  65. django_cfg/apps/payments/templates/payments/base.html +182 -0
  66. django_cfg/apps/payments/templates/payments/components/payment_card.html +201 -0
  67. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +109 -0
  68. django_cfg/apps/payments/templates/payments/components/progress_bar.html +43 -0
  69. django_cfg/apps/payments/templates/payments/components/provider_stats.html +40 -0
  70. django_cfg/apps/payments/templates/payments/components/status_badge.html +34 -0
  71. django_cfg/apps/payments/templates/payments/components/status_overview.html +148 -0
  72. django_cfg/apps/payments/templates/payments/dashboard.html +258 -0
  73. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
  74. django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
  75. django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
  76. django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
  77. django_cfg/apps/payments/templates/payments/stats.html +261 -0
  78. django_cfg/apps/payments/templates/payments/test.html +213 -0
  79. django_cfg/apps/payments/templatetags/__init__.py +1 -0
  80. django_cfg/apps/payments/templatetags/payments_tags.py +315 -0
  81. django_cfg/apps/payments/urls.py +3 -1
  82. django_cfg/apps/payments/urls_admin.py +58 -0
  83. django_cfg/apps/payments/utils/__init__.py +1 -3
  84. django_cfg/apps/payments/utils/billing_utils.py +2 -2
  85. django_cfg/apps/payments/utils/config_utils.py +2 -8
  86. django_cfg/apps/payments/utils/validation_utils.py +2 -2
  87. django_cfg/apps/payments/views/__init__.py +3 -2
  88. django_cfg/apps/payments/views/currency_views.py +31 -20
  89. django_cfg/apps/payments/views/payment_views.py +2 -2
  90. django_cfg/apps/payments/views/templates/__init__.py +25 -0
  91. django_cfg/apps/payments/views/templates/ajax.py +451 -0
  92. django_cfg/apps/payments/views/templates/base.py +212 -0
  93. django_cfg/apps/payments/views/templates/dashboard.py +60 -0
  94. django_cfg/apps/payments/views/templates/payment_detail.py +102 -0
  95. django_cfg/apps/payments/views/templates/payment_management.py +158 -0
  96. django_cfg/apps/payments/views/templates/qr_code.py +174 -0
  97. django_cfg/apps/payments/views/templates/stats.py +244 -0
  98. django_cfg/apps/payments/views/templates/utils.py +181 -0
  99. django_cfg/apps/payments/views/webhook_views.py +2 -2
  100. django_cfg/apps/payments/viewsets.py +3 -2
  101. django_cfg/apps/tasks/urls.py +0 -2
  102. django_cfg/apps/tasks/urls_admin.py +14 -0
  103. django_cfg/apps/urls.py +6 -3
  104. django_cfg/core/config.py +35 -0
  105. django_cfg/models/payments.py +2 -8
  106. django_cfg/modules/django_currency/__init__.py +16 -11
  107. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  108. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  109. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  110. django_cfg/modules/django_currency/core/__init__.py +1 -7
  111. django_cfg/modules/django_currency/core/converter.py +18 -23
  112. django_cfg/modules/django_currency/core/models.py +122 -11
  113. django_cfg/modules/django_currency/database/__init__.py +4 -4
  114. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  115. django_cfg/modules/django_unfold/dashboard.py +7 -2
  116. django_cfg/registry/core.py +1 -0
  117. django_cfg/template_archive/.gitignore +1 -0
  118. django_cfg/template_archive/django_sample.zip +0 -0
  119. django_cfg/templates/admin/components/action_grid.html +9 -9
  120. django_cfg/templates/admin/components/metric_card.html +5 -5
  121. django_cfg/templates/admin/components/status_badge.html +2 -2
  122. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  123. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  124. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  125. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  126. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/METADATA +13 -18
  127. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/RECORD +130 -83
  128. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  129. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  130. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  131. django_cfg/apps/payments/services/providers/cryptomus.py +0 -310
  132. django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
  133. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  134. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  135. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  136. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
  137. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
  138. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -1,273 +0,0 @@
1
- """
2
- CryptAPI provider implementation.
3
-
4
- Crypto payment provider using CryptAPI service.
5
- """
6
-
7
- import logging
8
- import requests
9
- import secrets
10
- import string
11
- from typing import Optional, List
12
- from decimal import Decimal
13
- from pydantic import BaseModel, Field
14
-
15
- from .base import PaymentProvider
16
- from ..internal_types import ProviderResponse, WebhookData
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
-
21
- class CryptAPIConfig(BaseModel):
22
- """CryptAPI provider configuration."""
23
- own_address: str = Field(..., description="Your cryptocurrency address")
24
- callback_url: str = Field(..., description="Webhook callback URL")
25
- convert_payments: bool = Field(default=True, description="Auto-convert payments")
26
- multi_token: bool = Field(default=True, description="Support multi-token payments")
27
- priority: str = Field(default='default', description="Transaction priority")
28
- enabled: bool = Field(default=True, description="Provider enabled")
29
-
30
-
31
- class CryptAPIException(Exception):
32
- """CryptAPI specific exception."""
33
- pass
34
-
35
-
36
- class CryptAPIProvider(PaymentProvider):
37
- """CryptAPI cryptocurrency payment provider."""
38
-
39
- CRYPTAPI_URL = 'https://api.cryptapi.io/'
40
-
41
- def __init__(self, config: CryptAPIConfig):
42
- """Initialize CryptAPI provider."""
43
- super().__init__(config.dict())
44
- self.config = config
45
- self.own_address = config.own_address
46
- self.callback_url = config.callback_url
47
- self.convert_payments = config.convert_payments
48
- self.multi_token = config.multi_token
49
- self.priority = config.priority
50
-
51
- def _make_request(self, coin: str, endpoint: str, params: Optional[dict] = None) -> Optional[dict]:
52
- """Make HTTP request to CryptAPI."""
53
- try:
54
- if coin:
55
- coin = coin.replace('/', '_')
56
- url = f"{self.CRYPTAPI_URL}{coin}/{endpoint}/"
57
- else:
58
- url = f"{self.CRYPTAPI_URL}{endpoint}/"
59
-
60
- response = requests.get(url, params=params or {}, timeout=30)
61
- response.raise_for_status()
62
-
63
- result = response.json()
64
-
65
- # Check for API errors
66
- if 'error' in result:
67
- logger.error(f"CryptAPI error: {result['error']}")
68
- return None
69
-
70
- return result
71
-
72
- except requests.exceptions.RequestException as e:
73
- logger.error(f"CryptAPI request failed: {e}")
74
- return None
75
- except Exception as e:
76
- logger.error(f"Unexpected CryptAPI error: {e}")
77
- return None
78
-
79
- def create_payment(self, payment_data: dict) -> ProviderResponse:
80
- """Create payment address via CryptAPI."""
81
- try:
82
- amount = Decimal(str(payment_data['amount']))
83
- currency = payment_data['currency'].lower()
84
- order_id = payment_data.get('order_id', f'payment_{int(amount * 100)}')
85
-
86
- # Generate secure nonce for replay attack protection
87
- security_nonce = self._generate_nonce()
88
-
89
- # Build callback URL with parameters including nonce
90
- callback_params = {
91
- 'order_id': order_id,
92
- 'amount': str(amount),
93
- 'nonce': security_nonce
94
- }
95
-
96
- # Create payment address
97
- params = {
98
- 'address': self.own_address,
99
- 'callback': self.callback_url,
100
- 'convert': 1 if self.convert_payments else 0,
101
- 'multi_token': 1 if self.multi_token else 0,
102
- 'priority': self.priority,
103
- **callback_params
104
- }
105
-
106
- response = self._make_request(currency, 'create', params)
107
-
108
- if response and 'address_in' in response:
109
- return ProviderResponse(
110
- success=True,
111
- provider_payment_id=response['address_in'], # Use address as payment ID
112
- payment_url=None, # CryptAPI doesn't provide payment URLs
113
- pay_address=response['address_in'],
114
- amount=amount,
115
- currency=currency.upper(),
116
- status='pending'
117
- )
118
- else:
119
- return ProviderResponse(
120
- success=False,
121
- error_message='Failed to create payment address'
122
- )
123
-
124
- except Exception as e:
125
- logger.error(f"CryptAPI create_payment error: {e}")
126
- return ProviderResponse(
127
- success=False,
128
- error_message=str(e)
129
- )
130
-
131
- def check_payment_status(self, payment_id: str) -> ProviderResponse:
132
- """Check payment status via CryptAPI."""
133
- try:
134
- # For CryptAPI, payment_id is the address
135
- # We need to check logs to see if payment was received
136
- # This is a limitation of CryptAPI - no direct status check by address
137
-
138
- # Return pending status as CryptAPI uses callbacks for status updates
139
- return ProviderResponse(
140
- success=True,
141
- provider_payment_id=payment_id,
142
- status='pending',
143
- pay_address=payment_id,
144
- amount=Decimal('0'), # Unknown without logs
145
- currency='unknown'
146
- )
147
-
148
- except Exception as e:
149
- logger.error(f"CryptAPI check_payment_status error: {e}")
150
- return ProviderResponse(
151
- success=False,
152
- error_message=str(e)
153
- )
154
-
155
- def process_webhook(self, payload: dict) -> WebhookData:
156
- """Process CryptAPI webhook/callback."""
157
- try:
158
- # CryptAPI sends callbacks with these parameters:
159
- # - address_in: payment address
160
- # - address_out: your address
161
- # - txid_in: transaction ID
162
- # - txid_out: forwarding transaction ID (if applicable)
163
- # - confirmations: number of confirmations
164
- # - value: amount received
165
- # - value_coin: amount in coin
166
- # - value_forwarded: amount forwarded
167
- # - coin: cryptocurrency
168
- # - pending: 0 or 1
169
-
170
- confirmations = int(payload.get('confirmations', 0))
171
- pending = int(payload.get('pending', 1))
172
-
173
- # Determine status based on confirmations and pending flag
174
- if pending == 1:
175
- status = 'pending'
176
- elif confirmations >= 1:
177
- status = 'completed'
178
- else:
179
- status = 'processing'
180
-
181
- return WebhookData(
182
- provider_payment_id=payload.get('address_in', ''),
183
- status=status,
184
- pay_amount=Decimal(str(payload.get('value_coin', 0))),
185
- actually_paid=Decimal(str(payload.get('value_coin', 0))),
186
- order_id=payload.get('order_id'), # Custom parameter we sent
187
- signature=payload.get('txid_in') # Use transaction ID as signature
188
- )
189
-
190
- except Exception as e:
191
- logger.error(f"CryptAPI webhook processing error: {e}")
192
- raise
193
-
194
- def get_supported_currencies(self) -> List[str]:
195
- """Get list of supported currencies."""
196
- try:
197
- response = self._make_request('', 'info')
198
-
199
- if response and isinstance(response, dict):
200
- # CryptAPI returns a dict with coin info
201
- return list(response.keys())
202
- else:
203
- # Fallback currencies
204
- return ['BTC', 'ETH', 'LTC', 'BCH', 'XMR', 'TRX']
205
-
206
- except Exception as e:
207
- logger.error(f"Error getting supported currencies: {e}")
208
- return ['BTC', 'ETH', 'LTC'] # Minimal fallback
209
-
210
- def get_minimum_payment_amount(self, currency_from: str, currency_to: str = 'usd') -> Optional[Decimal]:
211
- """Get minimum payment amount for currency."""
212
- try:
213
- response = self._make_request(currency_from.lower(), 'info')
214
-
215
- if response and 'minimum_transaction' in response:
216
- return Decimal(str(response['minimum_transaction']))
217
-
218
- return None
219
-
220
- except Exception as e:
221
- logger.error(f"Error getting minimum amount: {e}")
222
- return None
223
-
224
- def estimate_payment_amount(self, amount: Decimal, currency_code: str) -> Optional[dict]:
225
- """Estimate payment amount - CryptAPI doesn't provide this."""
226
- # CryptAPI doesn't have a direct estimation API
227
- # Would need to use external price APIs
228
- return None
229
-
230
- def validate_webhook(self, payload: dict, headers: Optional[dict] = None) -> bool:
231
- """Validate CryptAPI webhook."""
232
- try:
233
- # CryptAPI doesn't use HMAC signatures
234
- # Validation is done by checking if the callback came from their servers
235
- # and contains expected parameters
236
-
237
- required_fields = ['address_in', 'address_out', 'txid_in', 'value_coin', 'coin', 'confirmations']
238
-
239
- for field in required_fields:
240
- if field not in payload:
241
- logger.warning(f"Missing required field in CryptAPI webhook: {field}")
242
- return False
243
-
244
- # Basic validation passed
245
- return True
246
-
247
- except Exception as e:
248
- logger.error(f"CryptAPI webhook validation error: {e}")
249
- return False
250
-
251
- def check_api_status(self) -> bool:
252
- """Check if CryptAPI is available."""
253
- try:
254
- response = self._make_request('', 'info')
255
- return response is not None
256
- except:
257
- return False
258
-
259
- def get_logs(self, callback_url: str) -> Optional[dict]:
260
- """Get payment logs for a callback URL."""
261
- try:
262
- params = {'callback': callback_url}
263
- # Note: This would need a specific coin, but we don't know which one
264
- # This is a limitation of the current implementation
265
- return None
266
- except Exception as e:
267
- logger.error(f"Error getting logs: {e}")
268
- return None
269
-
270
- def _generate_nonce(self, length: int = 32) -> str:
271
- """Generate cryptographically secure nonce for replay attack protection."""
272
- sequence = string.ascii_letters + string.digits
273
- return ''.join([secrets.choice(sequence) for _ in range(length)])
@@ -1,310 +0,0 @@
1
- """
2
- Cryptomus payment provider implementation.
3
- """
4
-
5
- import logging
6
- import hashlib
7
- import json
8
- import base64
9
- from decimal import Decimal
10
- from typing import Dict, Any, Optional
11
- from dataclasses import dataclass
12
-
13
- import requests
14
- from pydantic import BaseModel, Field, validator
15
-
16
- from .base import PaymentProvider, ProviderResponse, ProviderConfig
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
-
21
- class CryptomusConfig(ProviderConfig):
22
- """Configuration for Cryptomus provider."""
23
-
24
- merchant_id: str = Field(..., description="Cryptomus merchant ID")
25
- api_key: str = Field(..., description="Cryptomus API key")
26
- test_mode: bool = Field(default=False, description="Enable test mode")
27
- callback_url: Optional[str] = Field(None, description="Default callback URL")
28
-
29
- @validator('merchant_id')
30
- def validate_merchant_id(cls, v):
31
- if not v or not v.strip():
32
- raise ValueError("Merchant ID is required")
33
- return v.strip()
34
-
35
- @validator('api_key')
36
- def validate_api_key(cls, v):
37
- if not v or len(v) < 10:
38
- raise ValueError("API key must be at least 10 characters")
39
- return v
40
-
41
-
42
- class CryptomusProvider(PaymentProvider):
43
- """Cryptomus payment provider with universal field mapping."""
44
-
45
- def __init__(self, config: CryptomusConfig):
46
- super().__init__(config)
47
- self.merchant_id = config.merchant_id
48
- self.api_key = config.api_key
49
- self.test_mode = config.test_mode
50
- self.base_url = "https://api.cryptomus.com/v1" if not config.test_mode else "https://api.cryptomus.com/v1"
51
-
52
- def create_payment(self, payment_data: dict) -> ProviderResponse:
53
- """
54
- Create payment using Cryptomus API.
55
- Maps to universal payment fields.
56
- """
57
- try:
58
- # Extract required data
59
- order_id = payment_data.get('order_id')
60
- amount = payment_data.get('amount')
61
- currency = payment_data.get('currency', 'USD')
62
- callback_url = payment_data.get('callback_url', self.config.callback_url)
63
-
64
- if not all([order_id, amount]):
65
- return ProviderResponse(
66
- success=False,
67
- error_message="Missing required fields: order_id, amount"
68
- )
69
-
70
- # Prepare Cryptomus API request
71
- payload = {
72
- "amount": str(amount),
73
- "currency": currency,
74
- "order_id": order_id,
75
- "url_callback": callback_url,
76
- "url_return": payment_data.get('return_url'),
77
- "url_success": payment_data.get('success_url'),
78
- "is_payment_multiple": False,
79
- "lifetime": 3600, # 1 hour
80
- "to_currency": payment_data.get('crypto_currency', 'BTC')
81
- }
82
-
83
- # Generate signature
84
- headers = self._generate_headers(payload)
85
-
86
- # Make API request
87
- response = requests.post(
88
- f"{self.base_url}/payment",
89
- json=payload,
90
- headers=headers,
91
- timeout=30
92
- )
93
-
94
- if response.status_code == 200:
95
- result = response.json()
96
-
97
- if result.get('state') == 0: # Success
98
- payment_info = result.get('result', {})
99
-
100
- return ProviderResponse(
101
- success=True,
102
- transaction_id=payment_info.get('uuid'),
103
- data={
104
- # Universal field mapping
105
- 'provider_payment_id': payment_info.get('uuid'),
106
- 'receiver_address': payment_info.get('address'),
107
- 'crypto_amount': float(payment_info.get('amount', 0)),
108
- 'provider_callback_url': callback_url,
109
- 'payment_url': payment_info.get('url'),
110
- 'qr_code': payment_info.get('static_qr'),
111
-
112
- # Cryptomus specific fields
113
- 'cryptomus_order_id': payment_info.get('order_id'),
114
- 'cryptomus_currency': payment_info.get('currency'),
115
- 'cryptomus_network': payment_info.get('network'),
116
- 'cryptomus_status': payment_info.get('status'),
117
- 'expires_at': payment_info.get('expired_at')
118
- }
119
- )
120
- else:
121
- error_msg = result.get('message', 'Unknown Cryptomus error')
122
- return ProviderResponse(
123
- success=False,
124
- error_message=f"Cryptomus API error: {error_msg}"
125
- )
126
- else:
127
- return ProviderResponse(
128
- success=False,
129
- error_message=f"HTTP {response.status_code}: {response.text}"
130
- )
131
-
132
- except requests.RequestException as e:
133
- logger.error(f"Cryptomus API request failed: {e}")
134
- return ProviderResponse(
135
- success=False,
136
- error_message=f"Network error: {str(e)}"
137
- )
138
- except Exception as e:
139
- logger.error(f"Cryptomus payment creation failed: {e}")
140
- return ProviderResponse(
141
- success=False,
142
- error_message=f"Unexpected error: {str(e)}"
143
- )
144
-
145
- def validate_webhook(self, webhook_data: Dict[str, Any],
146
- request_headers: Dict[str, str], raw_body: bytes) -> tuple[bool, Optional[str]]:
147
- """
148
- Validate Cryptomus webhook signature and required fields.
149
- """
150
- try:
151
- # Check required fields
152
- required_fields = ['uuid', 'order_id', 'amount', 'currency', 'status']
153
- for field in required_fields:
154
- if field not in webhook_data:
155
- return False, f"Missing required field: {field}"
156
-
157
- # Validate signature if provided
158
- sign = request_headers.get('sign') or webhook_data.get('sign')
159
- if sign:
160
- # Generate expected signature
161
- expected_sign = self._generate_webhook_signature(webhook_data)
162
- if sign != expected_sign:
163
- return False, "Invalid webhook signature"
164
-
165
- return True, None
166
-
167
- except Exception as e:
168
- logger.error(f"Cryptomus webhook validation failed: {e}")
169
- return False, f"Validation error: {str(e)}"
170
-
171
- def process_webhook(self, webhook_data: Dict[str, Any]) -> ProviderResponse:
172
- """
173
- Process Cryptomus webhook and map to universal fields.
174
- """
175
- try:
176
- # Map Cryptomus webhook fields to universal fields
177
- universal_data = {
178
- 'provider_payment_id': webhook_data.get('uuid'),
179
- 'status': self._map_status(webhook_data.get('status')),
180
- 'transaction_hash': webhook_data.get('txid'),
181
- 'sender_address': webhook_data.get('from'),
182
- 'receiver_address': webhook_data.get('to'),
183
- 'crypto_amount': float(webhook_data.get('amount', 0)),
184
- 'confirmations_count': int(webhook_data.get('confirmations', 0)),
185
-
186
- # Additional Cryptomus data
187
- 'cryptomus_network': webhook_data.get('network'),
188
- 'cryptomus_currency': webhook_data.get('currency'),
189
- 'cryptomus_commission': webhook_data.get('commission'),
190
- 'updated_at': webhook_data.get('updated_at')
191
- }
192
-
193
- return ProviderResponse(
194
- success=True,
195
- data=universal_data
196
- )
197
-
198
- except Exception as e:
199
- logger.error(f"Cryptomus webhook processing failed: {e}")
200
- return ProviderResponse(
201
- success=False,
202
- error_message=f"Webhook processing error: {str(e)}"
203
- )
204
-
205
- def get_payment_status(self, payment_id: str) -> ProviderResponse:
206
- """Get payment status from Cryptomus."""
207
- try:
208
- payload = {"uuid": payment_id}
209
- headers = self._generate_headers(payload)
210
-
211
- response = requests.post(
212
- f"{self.base_url}/payment/info",
213
- json=payload,
214
- headers=headers,
215
- timeout=30
216
- )
217
-
218
- if response.status_code == 200:
219
- result = response.json()
220
- if result.get('state') == 0:
221
- payment_info = result.get('result', {})
222
-
223
- return ProviderResponse(
224
- success=True,
225
- data={
226
- 'status': self._map_status(payment_info.get('status')),
227
- 'provider_payment_id': payment_info.get('uuid'),
228
- 'transaction_hash': payment_info.get('txid'),
229
- 'crypto_amount': float(payment_info.get('amount', 0)),
230
- 'confirmations_count': int(payment_info.get('confirmations', 0))
231
- }
232
- )
233
-
234
- return ProviderResponse(
235
- success=False,
236
- error_message="Failed to get payment status"
237
- )
238
-
239
- except Exception as e:
240
- logger.error(f"Cryptomus status check failed: {e}")
241
- return ProviderResponse(
242
- success=False,
243
- error_message=f"Status check error: {str(e)}"
244
- )
245
-
246
- def _generate_headers(self, payload: dict) -> dict:
247
- """Generate required headers for Cryptomus API."""
248
- data_string = base64.b64encode(json.dumps(payload).encode()).decode()
249
- sign = hashlib.md5(f"{data_string}{self.api_key}".encode()).hexdigest()
250
-
251
- return {
252
- "merchant": self.merchant_id,
253
- "sign": sign,
254
- "Content-Type": "application/json"
255
- }
256
-
257
- def _generate_webhook_signature(self, webhook_data: dict) -> str:
258
- """Generate expected webhook signature for validation."""
259
- # Cryptomus webhook signature generation
260
- data_string = base64.b64encode(json.dumps(webhook_data, sort_keys=True).encode()).decode()
261
- return hashlib.md5(f"{data_string}{self.api_key}".encode()).hexdigest()
262
-
263
- def _map_status(self, cryptomus_status: str) -> str:
264
- """Map Cryptomus status to universal status."""
265
- status_mapping = {
266
- 'check': 'pending',
267
- 'process': 'pending',
268
- 'confirm_check': 'pending',
269
- 'confirmed': 'completed',
270
- 'fail': 'failed',
271
- 'cancel': 'cancelled',
272
- 'system_fail': 'failed',
273
- 'refund_process': 'refunding',
274
- 'refund_fail': 'failed',
275
- 'refund_paid': 'refunded'
276
- }
277
- return status_mapping.get(cryptomus_status, 'pending')
278
-
279
- def get_supported_currencies(self) -> ProviderResponse:
280
- """Get supported currencies from Cryptomus."""
281
- try:
282
- headers = self._generate_headers({})
283
-
284
- response = requests.post(
285
- f"{self.base_url}/exchange-rate/list",
286
- json={},
287
- headers=headers,
288
- timeout=30
289
- )
290
-
291
- if response.status_code == 200:
292
- result = response.json()
293
- if result.get('state') == 0:
294
- currencies = result.get('result', [])
295
- return ProviderResponse(
296
- success=True,
297
- data={'currencies': currencies}
298
- )
299
-
300
- return ProviderResponse(
301
- success=False,
302
- error_message="Failed to get supported currencies"
303
- )
304
-
305
- except Exception as e:
306
- logger.error(f"Cryptomus currencies request failed: {e}")
307
- return ProviderResponse(
308
- success=False,
309
- error_message=f"Currencies request error: {str(e)}"
310
- )