django-cfg 1.2.23__py3-none-any.whl → 1.2.25__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/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- django_cfg/apps/payments/config/__init__.py +15 -37
- django_cfg/apps/payments/config/module.py +30 -122
- django_cfg/apps/payments/config/providers.py +22 -0
- django_cfg/apps/payments/config/settings.py +53 -93
- django_cfg/apps/payments/config/utils.py +10 -156
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +33 -0
- django_cfg/apps/payments/migrations/0001_initial.py +94 -1
- django_cfg/apps/payments/models/payments.py +110 -0
- django_cfg/apps/payments/services/__init__.py +7 -1
- django_cfg/apps/payments/services/core/balance_service.py +14 -16
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +212 -29
- django_cfg/apps/payments/services/core/subscription_service.py +15 -17
- django_cfg/apps/payments/services/internal_types.py +31 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +14 -3
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
- django_cfg/apps/payments/signals/api_key_signals.py +10 -0
- django_cfg/apps/payments/signals/payment_signals.py +3 -2
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/utils/__init__.py +7 -4
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +2 -0
- django_cfg/apps/payments/views/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +11 -5
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/METADATA +10 -6
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/RECORD +77 -51
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- django_cfg/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,372 @@
|
|
1
|
+
"""
|
2
|
+
Provider Health Monitoring System.
|
3
|
+
|
4
|
+
Monitors the health of all payment providers and provides
|
5
|
+
fallback mechanisms when providers are unavailable.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
import time
|
10
|
+
import asyncio
|
11
|
+
import requests
|
12
|
+
from typing import Dict, List, Optional, Any
|
13
|
+
from datetime import datetime, timedelta
|
14
|
+
from dataclasses import dataclass
|
15
|
+
from enum import Enum
|
16
|
+
|
17
|
+
from django.utils import timezone
|
18
|
+
from django.core.cache import cache
|
19
|
+
from pydantic import BaseModel, Field
|
20
|
+
|
21
|
+
from ..providers.registry import ProviderRegistry
|
22
|
+
from ...models.events import PaymentEvent
|
23
|
+
from .api_schemas import parse_provider_response
|
24
|
+
|
25
|
+
logger = logging.getLogger(__name__)
|
26
|
+
|
27
|
+
|
28
|
+
class HealthStatus(Enum):
|
29
|
+
"""Provider health status levels."""
|
30
|
+
HEALTHY = "healthy"
|
31
|
+
DEGRADED = "degraded"
|
32
|
+
UNHEALTHY = "unhealthy"
|
33
|
+
UNKNOWN = "unknown"
|
34
|
+
|
35
|
+
|
36
|
+
class ProviderHealthCheck(BaseModel):
|
37
|
+
"""Provider health check result."""
|
38
|
+
provider_name: str = Field(..., description="Provider name")
|
39
|
+
status: HealthStatus = Field(..., description="Health status")
|
40
|
+
response_time_ms: float = Field(..., description="Response time in milliseconds")
|
41
|
+
status_code: Optional[int] = Field(None, description="HTTP status code")
|
42
|
+
error_message: Optional[str] = Field(None, description="Error message if unhealthy")
|
43
|
+
checked_at: datetime = Field(default_factory=timezone.now, description="Check timestamp")
|
44
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
45
|
+
|
46
|
+
|
47
|
+
class ProviderHealthSummary(BaseModel):
|
48
|
+
"""Summary of all provider health statuses."""
|
49
|
+
total_providers: int = Field(..., description="Total number of providers")
|
50
|
+
healthy_count: int = Field(..., description="Number of healthy providers")
|
51
|
+
degraded_count: int = Field(..., description="Number of degraded providers")
|
52
|
+
unhealthy_count: int = Field(..., description="Number of unhealthy providers")
|
53
|
+
providers: List[ProviderHealthCheck] = Field(..., description="Individual provider health checks")
|
54
|
+
last_updated: datetime = Field(default_factory=timezone.now, description="Last update timestamp")
|
55
|
+
|
56
|
+
|
57
|
+
class ProviderHealthMonitor:
|
58
|
+
"""
|
59
|
+
Monitor the health of all payment providers.
|
60
|
+
|
61
|
+
Features:
|
62
|
+
- Real-time health checks
|
63
|
+
- Provider availability tracking
|
64
|
+
- Automatic fallback recommendations
|
65
|
+
- Health history and trends
|
66
|
+
- Alert system integration
|
67
|
+
"""
|
68
|
+
|
69
|
+
def __init__(self):
|
70
|
+
"""Initialize health monitor."""
|
71
|
+
self.provider_registry = ProviderRegistry()
|
72
|
+
self.cache_timeout = 300 # 5 minutes
|
73
|
+
self.health_check_timeout = 10 # 10 seconds
|
74
|
+
|
75
|
+
# Health check endpoints for each provider
|
76
|
+
self.health_endpoints = {
|
77
|
+
'cryptapi': 'https://api.cryptapi.io/btc/info/',
|
78
|
+
'cryptomus': 'https://api.cryptomus.com', # Base URL check (returns 204 No Content = healthy)
|
79
|
+
'nowpayments': 'https://api.nowpayments.io/v1/status',
|
80
|
+
'stripe': 'https://api.stripe.com/v1/account' # Will return auth error = healthy
|
81
|
+
}
|
82
|
+
|
83
|
+
def check_all_providers(self) -> ProviderHealthSummary:
|
84
|
+
"""
|
85
|
+
Check health of all registered providers.
|
86
|
+
|
87
|
+
Returns:
|
88
|
+
ProviderHealthSummary with all provider statuses
|
89
|
+
"""
|
90
|
+
providers = self.provider_registry.get_all_providers()
|
91
|
+
health_checks = []
|
92
|
+
|
93
|
+
for provider_name, provider_instance in providers.items():
|
94
|
+
try:
|
95
|
+
health_check = self.check_provider_health(provider_name)
|
96
|
+
health_checks.append(health_check)
|
97
|
+
except Exception as e:
|
98
|
+
logger.error(f"Failed to check health for {provider_name}: {e}")
|
99
|
+
health_checks.append(ProviderHealthCheck(
|
100
|
+
provider_name=provider_name,
|
101
|
+
status=HealthStatus.UNKNOWN,
|
102
|
+
response_time_ms=0.0,
|
103
|
+
error_message=str(e)
|
104
|
+
))
|
105
|
+
|
106
|
+
# Calculate summary
|
107
|
+
healthy_count = sum(1 for check in health_checks if check.status == HealthStatus.HEALTHY)
|
108
|
+
degraded_count = sum(1 for check in health_checks if check.status == HealthStatus.DEGRADED)
|
109
|
+
unhealthy_count = sum(1 for check in health_checks if check.status == HealthStatus.UNHEALTHY)
|
110
|
+
|
111
|
+
summary = ProviderHealthSummary(
|
112
|
+
total_providers=len(health_checks),
|
113
|
+
healthy_count=healthy_count,
|
114
|
+
degraded_count=degraded_count,
|
115
|
+
unhealthy_count=unhealthy_count,
|
116
|
+
providers=health_checks
|
117
|
+
)
|
118
|
+
|
119
|
+
# Cache summary
|
120
|
+
cache.set('provider_health_summary', summary.dict(), self.cache_timeout)
|
121
|
+
|
122
|
+
# Log health summary
|
123
|
+
logger.info(f"Provider health check completed: {healthy_count}/{len(health_checks)} healthy")
|
124
|
+
|
125
|
+
return summary
|
126
|
+
|
127
|
+
def check_provider_health(self, provider_name: str) -> ProviderHealthCheck:
|
128
|
+
"""
|
129
|
+
Check health of a specific provider.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
provider_name: Name of the provider to check
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
ProviderHealthCheck with health status
|
136
|
+
"""
|
137
|
+
# Check cache first
|
138
|
+
cache_key = f'provider_health_{provider_name}'
|
139
|
+
cached_result = cache.get(cache_key)
|
140
|
+
if cached_result:
|
141
|
+
return ProviderHealthCheck(**cached_result)
|
142
|
+
|
143
|
+
start_time = time.time()
|
144
|
+
health_check = None
|
145
|
+
|
146
|
+
try:
|
147
|
+
# Get health endpoint for provider
|
148
|
+
endpoint = self.health_endpoints.get(provider_name)
|
149
|
+
if not endpoint:
|
150
|
+
raise ValueError(f"No health endpoint configured for {provider_name}")
|
151
|
+
|
152
|
+
# Make health check request
|
153
|
+
response = requests.get(
|
154
|
+
endpoint,
|
155
|
+
timeout=self.health_check_timeout,
|
156
|
+
headers={'User-Agent': 'DjangoCFG-PaymentMonitor/1.0'}
|
157
|
+
)
|
158
|
+
|
159
|
+
response_time = (time.time() - start_time) * 1000
|
160
|
+
|
161
|
+
# Parse response using Pydantic schemas
|
162
|
+
response_body = response.text if response.text else ""
|
163
|
+
parsed_health = parse_provider_response(
|
164
|
+
provider_name=provider_name,
|
165
|
+
status_code=response.status_code,
|
166
|
+
response_body=response_body,
|
167
|
+
response_time_ms=response_time
|
168
|
+
)
|
169
|
+
|
170
|
+
# Convert to our HealthStatus enum
|
171
|
+
if parsed_health.is_healthy:
|
172
|
+
status = HealthStatus.HEALTHY
|
173
|
+
elif 400 <= response.status_code < 500:
|
174
|
+
status = HealthStatus.DEGRADED
|
175
|
+
else:
|
176
|
+
status = HealthStatus.UNHEALTHY
|
177
|
+
|
178
|
+
health_check = ProviderHealthCheck(
|
179
|
+
provider_name=provider_name,
|
180
|
+
status=status,
|
181
|
+
response_time_ms=round(response_time, 2),
|
182
|
+
status_code=response.status_code,
|
183
|
+
error_message=parsed_health.error_message,
|
184
|
+
metadata={
|
185
|
+
'endpoint': endpoint,
|
186
|
+
'response_size': len(response.content) if response.content else 0,
|
187
|
+
'parsed_response': parsed_health.parsed_response,
|
188
|
+
'pydantic_validated': True
|
189
|
+
}
|
190
|
+
)
|
191
|
+
|
192
|
+
except requests.exceptions.Timeout:
|
193
|
+
response_time = (time.time() - start_time) * 1000
|
194
|
+
health_check = ProviderHealthCheck(
|
195
|
+
provider_name=provider_name,
|
196
|
+
status=HealthStatus.UNHEALTHY,
|
197
|
+
response_time_ms=round(response_time, 2),
|
198
|
+
error_message="Request timeout"
|
199
|
+
)
|
200
|
+
|
201
|
+
except requests.exceptions.ConnectionError:
|
202
|
+
response_time = (time.time() - start_time) * 1000
|
203
|
+
health_check = ProviderHealthCheck(
|
204
|
+
provider_name=provider_name,
|
205
|
+
status=HealthStatus.UNHEALTHY,
|
206
|
+
response_time_ms=round(response_time, 2),
|
207
|
+
error_message="Connection error"
|
208
|
+
)
|
209
|
+
|
210
|
+
except Exception as e:
|
211
|
+
response_time = (time.time() - start_time) * 1000
|
212
|
+
health_check = ProviderHealthCheck(
|
213
|
+
provider_name=provider_name,
|
214
|
+
status=HealthStatus.UNKNOWN,
|
215
|
+
response_time_ms=round(response_time, 2),
|
216
|
+
error_message=str(e)
|
217
|
+
)
|
218
|
+
|
219
|
+
# Cache result
|
220
|
+
cache.set(cache_key, health_check.dict(), self.cache_timeout // 2) # Shorter cache for individual checks
|
221
|
+
|
222
|
+
# Log health check
|
223
|
+
logger.info(f"Health check for {provider_name}: {health_check.status.value} ({health_check.response_time_ms}ms)")
|
224
|
+
|
225
|
+
return health_check
|
226
|
+
|
227
|
+
def get_healthy_providers(self) -> List[str]:
|
228
|
+
"""
|
229
|
+
Get list of currently healthy provider names.
|
230
|
+
|
231
|
+
Returns:
|
232
|
+
List of healthy provider names
|
233
|
+
"""
|
234
|
+
summary = self.check_all_providers()
|
235
|
+
return [
|
236
|
+
provider.provider_name
|
237
|
+
for provider in summary.providers
|
238
|
+
if provider.status == HealthStatus.HEALTHY
|
239
|
+
]
|
240
|
+
|
241
|
+
def get_fallback_provider(self, preferred_provider: str) -> Optional[str]:
|
242
|
+
"""
|
243
|
+
Get fallback provider when preferred provider is unhealthy.
|
244
|
+
|
245
|
+
Args:
|
246
|
+
preferred_provider: Name of preferred provider
|
247
|
+
|
248
|
+
Returns:
|
249
|
+
Name of healthy fallback provider or None
|
250
|
+
"""
|
251
|
+
healthy_providers = self.get_healthy_providers()
|
252
|
+
|
253
|
+
# Remove preferred provider from list
|
254
|
+
fallback_providers = [p for p in healthy_providers if p != preferred_provider]
|
255
|
+
|
256
|
+
if not fallback_providers:
|
257
|
+
logger.warning(f"No healthy fallback providers available for {preferred_provider}")
|
258
|
+
return None
|
259
|
+
|
260
|
+
# Return first healthy provider as fallback
|
261
|
+
fallback = fallback_providers[0]
|
262
|
+
logger.info(f"Fallback provider for {preferred_provider}: {fallback}")
|
263
|
+
|
264
|
+
return fallback
|
265
|
+
|
266
|
+
def record_provider_incident(self, provider_name: str, incident_type: str, details: Dict[str, Any]):
|
267
|
+
"""
|
268
|
+
Record provider incident for tracking.
|
269
|
+
|
270
|
+
Args:
|
271
|
+
provider_name: Name of the provider
|
272
|
+
incident_type: Type of incident (outage, degradation, etc.)
|
273
|
+
details: Incident details
|
274
|
+
"""
|
275
|
+
try:
|
276
|
+
PaymentEvent.objects.create(
|
277
|
+
payment_id=f"health_monitor_{timezone.now().timestamp()}",
|
278
|
+
event_type='provider_incident',
|
279
|
+
sequence_number=1,
|
280
|
+
event_data={
|
281
|
+
'provider_name': provider_name,
|
282
|
+
'incident_type': incident_type,
|
283
|
+
'details': details,
|
284
|
+
'timestamp': timezone.now().isoformat()
|
285
|
+
},
|
286
|
+
processed_by='health_monitor',
|
287
|
+
idempotency_key=f"incident_{provider_name}_{timezone.now().timestamp()}"
|
288
|
+
)
|
289
|
+
|
290
|
+
logger.warning(f"Provider incident recorded: {provider_name} - {incident_type}")
|
291
|
+
|
292
|
+
except Exception as e:
|
293
|
+
logger.error(f"Failed to record provider incident: {e}")
|
294
|
+
|
295
|
+
def get_provider_uptime(self, provider_name: str, days: int = 7) -> float:
|
296
|
+
"""
|
297
|
+
Calculate provider uptime percentage over specified period.
|
298
|
+
|
299
|
+
Args:
|
300
|
+
provider_name: Name of the provider
|
301
|
+
days: Number of days to calculate uptime for
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
Uptime percentage (0.0 to 100.0)
|
305
|
+
"""
|
306
|
+
try:
|
307
|
+
# Get provider incidents from last N days
|
308
|
+
since_date = timezone.now() - timedelta(days=days)
|
309
|
+
|
310
|
+
incidents = PaymentEvent.objects.filter(
|
311
|
+
event_type='provider_incident',
|
312
|
+
event_data__provider_name=provider_name,
|
313
|
+
created_at__gte=since_date
|
314
|
+
).count()
|
315
|
+
|
316
|
+
# Simple uptime calculation (this could be more sophisticated)
|
317
|
+
total_checks = days * 24 * 12 # Assume checks every 5 minutes
|
318
|
+
uptime_percentage = max(0.0, ((total_checks - incidents) / total_checks) * 100.0)
|
319
|
+
|
320
|
+
return round(uptime_percentage, 2)
|
321
|
+
|
322
|
+
except Exception as e:
|
323
|
+
logger.error(f"Failed to calculate uptime for {provider_name}: {e}")
|
324
|
+
return 0.0
|
325
|
+
|
326
|
+
def generate_health_report(self) -> Dict[str, Any]:
|
327
|
+
"""
|
328
|
+
Generate comprehensive health report.
|
329
|
+
|
330
|
+
Returns:
|
331
|
+
Dict with health report data
|
332
|
+
"""
|
333
|
+
summary = self.check_all_providers()
|
334
|
+
|
335
|
+
report = {
|
336
|
+
'summary': summary.dict(),
|
337
|
+
'uptime_stats': {},
|
338
|
+
'recommendations': [],
|
339
|
+
'generated_at': timezone.now().isoformat()
|
340
|
+
}
|
341
|
+
|
342
|
+
# Calculate uptime for each provider
|
343
|
+
for provider in summary.providers:
|
344
|
+
uptime = self.get_provider_uptime(provider.provider_name)
|
345
|
+
report['uptime_stats'][provider.provider_name] = uptime
|
346
|
+
|
347
|
+
# Generate recommendations
|
348
|
+
if summary.unhealthy_count > 0:
|
349
|
+
unhealthy_providers = [p.provider_name for p in summary.providers if p.status == HealthStatus.UNHEALTHY]
|
350
|
+
report['recommendations'].append({
|
351
|
+
'type': 'critical',
|
352
|
+
'message': f"Unhealthy providers detected: {', '.join(unhealthy_providers)}",
|
353
|
+
'action': 'Check provider API status and credentials'
|
354
|
+
})
|
355
|
+
|
356
|
+
if summary.healthy_count < 2:
|
357
|
+
report['recommendations'].append({
|
358
|
+
'type': 'warning',
|
359
|
+
'message': 'Low number of healthy providers',
|
360
|
+
'action': 'Consider adding additional payment provider integrations'
|
361
|
+
})
|
362
|
+
|
363
|
+
return report
|
364
|
+
|
365
|
+
|
366
|
+
# Global health monitor instance
|
367
|
+
health_monitor = ProviderHealthMonitor()
|
368
|
+
|
369
|
+
|
370
|
+
def get_health_monitor() -> ProviderHealthMonitor:
|
371
|
+
"""Get global health monitor instance."""
|
372
|
+
return health_monitor
|
@@ -8,6 +8,7 @@ from .base import PaymentProvider
|
|
8
8
|
from .registry import ProviderRegistry
|
9
9
|
from .nowpayments import NowPaymentsProvider, NowPaymentsConfig
|
10
10
|
from .cryptapi import CryptAPIProvider, CryptAPIConfig
|
11
|
+
from .cryptomus import CryptomusProvider, CryptomusConfig
|
11
12
|
|
12
13
|
__all__ = [
|
13
14
|
'PaymentProvider',
|
@@ -16,4 +17,6 @@ __all__ = [
|
|
16
17
|
'NowPaymentsConfig',
|
17
18
|
'CryptAPIProvider',
|
18
19
|
'CryptAPIConfig',
|
20
|
+
'CryptomusProvider',
|
21
|
+
'CryptomusConfig',
|
19
22
|
]
|
@@ -6,6 +6,8 @@ Crypto payment provider using CryptAPI service.
|
|
6
6
|
|
7
7
|
import logging
|
8
8
|
import requests
|
9
|
+
import secrets
|
10
|
+
import string
|
9
11
|
from typing import Optional, List
|
10
12
|
from decimal import Decimal
|
11
13
|
from pydantic import BaseModel, Field
|
@@ -81,10 +83,14 @@ class CryptAPIProvider(PaymentProvider):
|
|
81
83
|
currency = payment_data['currency'].lower()
|
82
84
|
order_id = payment_data.get('order_id', f'payment_{int(amount * 100)}')
|
83
85
|
|
84
|
-
#
|
86
|
+
# Generate secure nonce for replay attack protection
|
87
|
+
security_nonce = self._generate_nonce()
|
88
|
+
|
89
|
+
# Build callback URL with parameters including nonce
|
85
90
|
callback_params = {
|
86
91
|
'order_id': order_id,
|
87
|
-
'amount': str(amount)
|
92
|
+
'amount': str(amount),
|
93
|
+
'nonce': security_nonce
|
88
94
|
}
|
89
95
|
|
90
96
|
# Create payment address
|
@@ -228,7 +234,7 @@ class CryptAPIProvider(PaymentProvider):
|
|
228
234
|
# Validation is done by checking if the callback came from their servers
|
229
235
|
# and contains expected parameters
|
230
236
|
|
231
|
-
required_fields = ['address_in', '
|
237
|
+
required_fields = ['address_in', 'address_out', 'txid_in', 'value_coin', 'coin', 'confirmations']
|
232
238
|
|
233
239
|
for field in required_fields:
|
234
240
|
if field not in payload:
|
@@ -260,3 +266,8 @@ class CryptAPIProvider(PaymentProvider):
|
|
260
266
|
except Exception as e:
|
261
267
|
logger.error(f"Error getting logs: {e}")
|
262
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)])
|