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.
- 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/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 +4 -4
- django_cfg/apps/payments/templates/payments/components/payment_card.html +6 -6
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +4 -4
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +14 -7
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +2 -2
- django_cfg/apps/payments/templates/payments/components/status_badge.html +8 -1
- django_cfg/apps/payments/templates/payments/components/status_overview.html +34 -30
- django_cfg/apps/payments/templates/payments/dashboard.html +202 -290
- 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/urls.py +3 -1
- django_cfg/apps/payments/{urls_templates.py → urls_admin.py} +6 -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/ajax.py +141 -2
- django_cfg/apps/payments/views/templates/base.py +21 -13
- django_cfg/apps/payments/views/templates/payment_detail.py +1 -1
- django_cfg/apps/payments/views/templates/payment_management.py +34 -40
- django_cfg/apps/payments/views/templates/stats.py +8 -4
- 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 +4 -4
- 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/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.29.dist-info → django_cfg-1.2.31.dist-info}/METADATA +2 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/RECORD +118 -96
- 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 -311
- 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.29.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -4,30 +4,42 @@ Provider registry for managing payment providers.
|
|
4
4
|
Central registry with lazy loading and typed configuration.
|
5
5
|
"""
|
6
6
|
|
7
|
-
import
|
8
|
-
from typing import Optional, List
|
7
|
+
import time
|
8
|
+
from typing import Optional, List, Dict, Any
|
9
|
+
from datetime import datetime, timedelta
|
10
|
+
from django.core.cache import cache
|
11
|
+
from django.utils import timezone
|
9
12
|
|
10
13
|
from .base import PaymentProvider
|
11
|
-
from .
|
12
|
-
from .
|
13
|
-
from .
|
14
|
+
from ...utils.config_utils import get_payments_config
|
15
|
+
from .nowpayments.provider import NowPaymentsProvider
|
16
|
+
from .nowpayments.models import NowPaymentsConfig
|
17
|
+
from .cryptapi.provider import CryptAPIProvider
|
18
|
+
from .cryptapi.models import CryptAPIConfig
|
19
|
+
from .cryptomus.provider import CryptomusProvider
|
20
|
+
from .cryptomus.models import CryptomusConfig
|
21
|
+
from .stripe.provider import StripeProvider
|
22
|
+
from .stripe.models import StripeConfig
|
23
|
+
from django_cfg.modules.django_logger import get_logger
|
14
24
|
|
15
|
-
logger =
|
25
|
+
logger = get_logger("provider_registry")
|
16
26
|
|
17
27
|
|
18
28
|
class ProviderRegistry:
|
19
29
|
"""Central registry for payment providers with typed configs."""
|
20
30
|
|
21
31
|
def __init__(self):
|
22
|
-
"""Initialize registry with lazy loading."""
|
32
|
+
"""Initialize registry with lazy loading and health monitoring."""
|
23
33
|
self._providers: dict[str, PaymentProvider] = {}
|
24
34
|
self._provider_configs: dict[str, dict] = {}
|
35
|
+
self._health_cache: dict[str, dict] = {}
|
36
|
+
self._fallback_order: List[str] = [] # Provider preference order
|
25
37
|
self._load_configurations()
|
38
|
+
self._initialize_health_monitoring()
|
26
39
|
|
27
40
|
def _load_configurations(self) -> None:
|
28
41
|
"""Load provider configurations."""
|
29
42
|
try:
|
30
|
-
from ...utils.config_utils import get_payments_config
|
31
43
|
config = get_payments_config()
|
32
44
|
|
33
45
|
self._provider_configs = {}
|
@@ -52,8 +64,8 @@ class ProviderRegistry:
|
|
52
64
|
config = CryptomusConfig(**config_dict)
|
53
65
|
return CryptomusProvider(config)
|
54
66
|
elif name == 'stripe':
|
55
|
-
|
56
|
-
return
|
67
|
+
config = StripeConfig(**config_dict)
|
68
|
+
return StripeProvider(config)
|
57
69
|
else:
|
58
70
|
logger.warning(f"Unknown provider type: {name}")
|
59
71
|
return None
|
@@ -92,12 +104,283 @@ class ProviderRegistry:
|
|
92
104
|
active = []
|
93
105
|
for name in self.list_providers():
|
94
106
|
provider = self.get_provider(name)
|
95
|
-
if provider and provider.
|
107
|
+
if provider and provider.enabled:
|
96
108
|
active.append(name)
|
97
109
|
return active
|
98
110
|
|
111
|
+
def _initialize_health_monitoring(self) -> None:
|
112
|
+
"""Initialize health monitoring for all providers."""
|
113
|
+
try:
|
114
|
+
# Set up fallback order based on configuration or defaults
|
115
|
+
available_providers = self.list_providers()
|
116
|
+
|
117
|
+
# Default priority: NowPayments -> CryptAPI -> Cryptomus -> Stripe
|
118
|
+
priority_order = ['nowpayments', 'cryptapi', 'cryptomus', 'stripe']
|
119
|
+
|
120
|
+
self._fallback_order = [p for p in priority_order if p in available_providers]
|
121
|
+
|
122
|
+
# Add any other providers not in priority list
|
123
|
+
for provider in available_providers:
|
124
|
+
if provider not in self._fallback_order:
|
125
|
+
self._fallback_order.append(provider)
|
126
|
+
|
127
|
+
logger.info(f"Initialized provider fallback order: {self._fallback_order}")
|
128
|
+
|
129
|
+
except Exception as e:
|
130
|
+
logger.error(f"Error initializing health monitoring: {e}")
|
131
|
+
self._fallback_order = []
|
132
|
+
|
133
|
+
async def health_check_all(self) -> Dict[str, Dict[str, Any]]:
|
134
|
+
"""
|
135
|
+
Check health of all providers with performance metrics.
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
Dict mapping provider names to health status
|
139
|
+
"""
|
140
|
+
results = {}
|
141
|
+
|
142
|
+
for provider_name in self.list_providers():
|
143
|
+
try:
|
144
|
+
provider = self.get_provider(provider_name)
|
145
|
+
if not provider:
|
146
|
+
results[provider_name] = {
|
147
|
+
'status': 'unavailable',
|
148
|
+
'error': 'Provider not loaded',
|
149
|
+
'last_check': timezone.now().isoformat()
|
150
|
+
}
|
151
|
+
continue
|
152
|
+
|
153
|
+
# Measure response time
|
154
|
+
start_time = time.time()
|
155
|
+
|
156
|
+
try:
|
157
|
+
# Use get_supported_currencies as health check endpoint
|
158
|
+
health_response = provider.get_supported_currencies()
|
159
|
+
response_time = int((time.time() - start_time) * 1000) # ms
|
160
|
+
|
161
|
+
if health_response.success:
|
162
|
+
status = 'healthy'
|
163
|
+
error = None
|
164
|
+
else:
|
165
|
+
status = 'degraded'
|
166
|
+
error = health_response.error_message
|
167
|
+
|
168
|
+
except Exception as provider_error:
|
169
|
+
response_time = int((time.time() - start_time) * 1000) # ms
|
170
|
+
status = 'unhealthy'
|
171
|
+
error = str(provider_error)
|
172
|
+
|
173
|
+
# Cache health status
|
174
|
+
health_data = {
|
175
|
+
'status': status,
|
176
|
+
'response_time_ms': response_time,
|
177
|
+
'error': error,
|
178
|
+
'last_check': timezone.now().isoformat(),
|
179
|
+
'provider_enabled': provider.enabled
|
180
|
+
}
|
181
|
+
|
182
|
+
# Store in cache for 5 minutes
|
183
|
+
cache_key = f"provider_health:{provider_name}"
|
184
|
+
cache.set(cache_key, health_data, timeout=300)
|
185
|
+
|
186
|
+
results[provider_name] = health_data
|
187
|
+
|
188
|
+
except Exception as e:
|
189
|
+
logger.error(f"Health check failed for {provider_name}: {e}")
|
190
|
+
results[provider_name] = {
|
191
|
+
'status': 'error',
|
192
|
+
'error': str(e),
|
193
|
+
'last_check': timezone.now().isoformat()
|
194
|
+
}
|
195
|
+
|
196
|
+
return results
|
197
|
+
|
198
|
+
def get_healthy_providers(self, operation: str = None) -> List[str]:
|
199
|
+
"""
|
200
|
+
Get list of healthy providers in fallback order.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
operation: Specific operation (e.g., 'payment_creation', 'webhook')
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
List of provider names sorted by health and priority
|
207
|
+
"""
|
208
|
+
healthy_providers = []
|
209
|
+
|
210
|
+
for provider_name in self._fallback_order:
|
211
|
+
# Check cached health status
|
212
|
+
cache_key = f"provider_health:{provider_name}"
|
213
|
+
health_data = cache.get(cache_key)
|
214
|
+
|
215
|
+
if health_data and health_data.get('status') in ['healthy', 'degraded']:
|
216
|
+
provider = self.get_provider(provider_name)
|
217
|
+
if provider and provider.enabled:
|
218
|
+
healthy_providers.append(provider_name)
|
219
|
+
|
220
|
+
return healthy_providers
|
221
|
+
|
222
|
+
def get_provider_with_fallback(self, preferred_provider: str = None, operation: str = None) -> Optional[PaymentProvider]:
|
223
|
+
"""
|
224
|
+
Get provider with automatic fallback to healthy alternatives.
|
225
|
+
|
226
|
+
Args:
|
227
|
+
preferred_provider: Preferred provider name
|
228
|
+
operation: Operation type for provider selection
|
229
|
+
|
230
|
+
Returns:
|
231
|
+
PaymentProvider instance or None if all providers are down
|
232
|
+
"""
|
233
|
+
# Start with preferred provider if specified and healthy
|
234
|
+
if preferred_provider:
|
235
|
+
provider = self.get_provider(preferred_provider)
|
236
|
+
if provider and provider.enabled:
|
237
|
+
# Quick health check from cache
|
238
|
+
cache_key = f"provider_health:{preferred_provider}"
|
239
|
+
health_data = cache.get(cache_key)
|
240
|
+
|
241
|
+
if not health_data or health_data.get('status') in ['healthy', 'degraded']:
|
242
|
+
logger.info(f"Using preferred provider: {preferred_provider}")
|
243
|
+
return provider
|
244
|
+
else:
|
245
|
+
logger.warning(f"Preferred provider {preferred_provider} is unhealthy, falling back")
|
246
|
+
|
247
|
+
# Fallback to healthy providers in order
|
248
|
+
healthy_providers = self.get_healthy_providers(operation)
|
249
|
+
|
250
|
+
for provider_name in healthy_providers:
|
251
|
+
provider = self.get_provider(provider_name)
|
252
|
+
if provider:
|
253
|
+
logger.info(f"Using fallback provider: {provider_name}")
|
254
|
+
return provider
|
255
|
+
|
256
|
+
logger.error("No healthy providers available!")
|
257
|
+
return None
|
258
|
+
|
259
|
+
def record_provider_performance(self, provider_name: str, operation: str,
|
260
|
+
response_time_ms: int, success: bool) -> None:
|
261
|
+
"""
|
262
|
+
Record provider performance metrics.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
provider_name: Name of the provider
|
266
|
+
operation: Operation performed (e.g., 'create_payment', 'check_status')
|
267
|
+
response_time_ms: Response time in milliseconds
|
268
|
+
success: Whether operation was successful
|
269
|
+
"""
|
270
|
+
try:
|
271
|
+
# Store performance metrics in cache
|
272
|
+
metric_key = f"provider_metrics:{provider_name}:{operation}"
|
273
|
+
|
274
|
+
# Get current metrics
|
275
|
+
current_metrics = cache.get(metric_key, {
|
276
|
+
'total_requests': 0,
|
277
|
+
'successful_requests': 0,
|
278
|
+
'average_response_time': 0,
|
279
|
+
'last_updated': timezone.now().isoformat()
|
280
|
+
})
|
281
|
+
|
282
|
+
# Update metrics
|
283
|
+
total_requests = current_metrics['total_requests'] + 1
|
284
|
+
successful_requests = current_metrics['successful_requests'] + (1 if success else 0)
|
285
|
+
|
286
|
+
# Calculate rolling average response time
|
287
|
+
current_avg = current_metrics['average_response_time']
|
288
|
+
new_avg = ((current_avg * current_metrics['total_requests']) + response_time_ms) / total_requests
|
289
|
+
|
290
|
+
updated_metrics = {
|
291
|
+
'total_requests': total_requests,
|
292
|
+
'successful_requests': successful_requests,
|
293
|
+
'success_rate': (successful_requests / total_requests) * 100,
|
294
|
+
'average_response_time': int(new_avg),
|
295
|
+
'last_response_time': response_time_ms,
|
296
|
+
'last_updated': timezone.now().isoformat()
|
297
|
+
}
|
298
|
+
|
299
|
+
# Store for 24 hours
|
300
|
+
cache.set(metric_key, updated_metrics, timeout=86400)
|
301
|
+
|
302
|
+
# Log performance issues
|
303
|
+
if not success:
|
304
|
+
logger.warning(f"Provider {provider_name} operation {operation} failed (response time: {response_time_ms}ms)")
|
305
|
+
elif response_time_ms > 5000: # > 5 seconds
|
306
|
+
logger.warning(f"Provider {provider_name} operation {operation} slow (response time: {response_time_ms}ms)")
|
307
|
+
|
308
|
+
except Exception as e:
|
309
|
+
logger.error(f"Error recording provider performance: {e}")
|
310
|
+
|
311
|
+
def get_provider_metrics(self, provider_name: str = None) -> Dict[str, Dict[str, Any]]:
|
312
|
+
"""
|
313
|
+
Get performance metrics for providers.
|
314
|
+
|
315
|
+
Args:
|
316
|
+
provider_name: Specific provider or None for all providers
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
Dict of provider metrics
|
320
|
+
"""
|
321
|
+
if provider_name:
|
322
|
+
providers_to_check = [provider_name]
|
323
|
+
else:
|
324
|
+
providers_to_check = self.list_providers()
|
325
|
+
|
326
|
+
metrics = {}
|
327
|
+
|
328
|
+
for provider in providers_to_check:
|
329
|
+
provider_metrics = {}
|
330
|
+
|
331
|
+
# Common operations to check
|
332
|
+
operations = ['create_payment', 'check_status', 'process_webhook', 'get_currencies']
|
333
|
+
|
334
|
+
for operation in operations:
|
335
|
+
metric_key = f"provider_metrics:{provider}:{operation}"
|
336
|
+
operation_metrics = cache.get(metric_key)
|
337
|
+
|
338
|
+
if operation_metrics:
|
339
|
+
provider_metrics[operation] = operation_metrics
|
340
|
+
|
341
|
+
if provider_metrics:
|
342
|
+
metrics[provider] = provider_metrics
|
343
|
+
|
344
|
+
return metrics
|
345
|
+
|
99
346
|
def reload_providers(self) -> None:
|
100
347
|
"""Reload all providers from configuration."""
|
101
348
|
logger.info("Reloading providers from configuration")
|
102
349
|
self._providers.clear()
|
103
350
|
self._load_configurations()
|
351
|
+
|
352
|
+
|
353
|
+
# Global singleton instance
|
354
|
+
_registry_instance = None
|
355
|
+
|
356
|
+
def get_provider_registry() -> ProviderRegistry:
|
357
|
+
"""Get global provider registry instance."""
|
358
|
+
global _registry_instance
|
359
|
+
if _registry_instance is None:
|
360
|
+
_registry_instance = ProviderRegistry()
|
361
|
+
return _registry_instance
|
362
|
+
|
363
|
+
|
364
|
+
def get_payment_provider(provider_name: str) -> Optional[PaymentProvider]:
|
365
|
+
"""
|
366
|
+
Get payment provider instance by name.
|
367
|
+
|
368
|
+
Args:
|
369
|
+
provider_name: Name of provider (e.g. 'nowpayments', 'stripe')
|
370
|
+
|
371
|
+
Returns:
|
372
|
+
Provider instance or None if not found
|
373
|
+
"""
|
374
|
+
registry = get_provider_registry()
|
375
|
+
return registry.get_provider(provider_name)
|
376
|
+
|
377
|
+
|
378
|
+
def get_available_providers() -> List[str]:
|
379
|
+
"""
|
380
|
+
Get list of available provider names.
|
381
|
+
|
382
|
+
Returns:
|
383
|
+
List of provider names that are configured
|
384
|
+
"""
|
385
|
+
registry = get_provider_registry()
|
386
|
+
return registry.list_providers()
|
@@ -0,0 +1,184 @@
|
|
1
|
+
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
2
|
+
from typing import Optional, List, Dict, Any
|
3
|
+
from decimal import Decimal
|
4
|
+
|
5
|
+
from ...internal_types import ProviderConfig
|
6
|
+
|
7
|
+
|
8
|
+
class StripeConfig(ProviderConfig):
|
9
|
+
"""Stripe provider configuration with Pydantic v2."""
|
10
|
+
|
11
|
+
webhook_secret: Optional[str] = Field(None, description="Webhook endpoint secret")
|
12
|
+
success_url: Optional[str] = Field(None, description="Payment success redirect URL")
|
13
|
+
cancel_url: Optional[str] = Field(None, description="Payment cancel redirect URL")
|
14
|
+
|
15
|
+
@field_validator('api_key')
|
16
|
+
@classmethod
|
17
|
+
def validate_api_key(cls, v: str) -> str:
|
18
|
+
if not v or not v.startswith(('sk_test_', 'sk_live_')):
|
19
|
+
raise ValueError("Stripe API key must start with sk_test_ or sk_live_")
|
20
|
+
return v
|
21
|
+
|
22
|
+
|
23
|
+
class StripeCurrency(BaseModel):
|
24
|
+
"""Stripe specific currency model."""
|
25
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
26
|
+
|
27
|
+
currency_code: str = Field(..., description="Currency symbol (e.g., USD, EUR)")
|
28
|
+
name: str = Field(..., description="Full currency name")
|
29
|
+
decimal_digits: int = Field(..., description="Number of decimal digits")
|
30
|
+
min_amount: Optional[Decimal] = Field(None, description="Minimum charge amount")
|
31
|
+
max_amount: Optional[Decimal] = Field(None, description="Maximum charge amount")
|
32
|
+
is_zero_decimal: bool = Field(False, description="Zero-decimal currency (like JPY)")
|
33
|
+
|
34
|
+
|
35
|
+
class StripeNetwork(BaseModel):
|
36
|
+
"""Stripe network model (not applicable for fiat payments)."""
|
37
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
38
|
+
|
39
|
+
code: str = Field(..., description="Network code (always 'stripe')")
|
40
|
+
name: str = Field(..., description="Network display name")
|
41
|
+
currency: str = Field(..., description="Currency this network belongs to")
|
42
|
+
|
43
|
+
|
44
|
+
class StripePaymentIntentRequest(BaseModel):
|
45
|
+
"""Stripe PaymentIntent creation request."""
|
46
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
47
|
+
|
48
|
+
amount: int = Field(..., description="Amount in smallest currency unit")
|
49
|
+
currency: str = Field(..., description="Three-letter ISO currency code")
|
50
|
+
payment_method_types: List[str] = Field(default=["card"], description="Payment method types")
|
51
|
+
confirm: bool = Field(False, description="Confirm PaymentIntent immediately")
|
52
|
+
capture_method: str = Field("automatic", description="Capture method")
|
53
|
+
confirmation_method: str = Field("automatic", description="Confirmation method")
|
54
|
+
description: Optional[str] = Field(None, description="Payment description")
|
55
|
+
metadata: Optional[Dict[str, str]] = Field(None, description="Metadata")
|
56
|
+
receipt_email: Optional[str] = Field(None, description="Receipt email")
|
57
|
+
return_url: Optional[str] = Field(None, description="Return URL")
|
58
|
+
success_url: Optional[str] = Field(None, description="Success URL")
|
59
|
+
cancel_url: Optional[str] = Field(None, description="Cancel URL")
|
60
|
+
|
61
|
+
|
62
|
+
class StripePaymentIntentResponse(BaseModel):
|
63
|
+
"""Stripe PaymentIntent response."""
|
64
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
65
|
+
|
66
|
+
id: str = Field(..., description="PaymentIntent ID")
|
67
|
+
object: str = Field(..., description="Object type")
|
68
|
+
amount: int = Field(..., description="Amount in smallest currency unit")
|
69
|
+
currency: str = Field(..., description="Currency code")
|
70
|
+
status: str = Field(..., description="PaymentIntent status")
|
71
|
+
client_secret: str = Field(..., description="Client secret for confirmation")
|
72
|
+
created: int = Field(..., description="Creation timestamp")
|
73
|
+
description: Optional[str] = Field(None, description="Payment description")
|
74
|
+
metadata: Dict[str, str] = Field(default_factory=dict, description="Metadata")
|
75
|
+
payment_method: Optional[str] = Field(None, description="Payment method ID")
|
76
|
+
receipt_email: Optional[str] = Field(None, description="Receipt email")
|
77
|
+
latest_charge: Optional[str] = Field(None, description="Latest charge ID")
|
78
|
+
|
79
|
+
|
80
|
+
class StripeWebhookEvent(BaseModel):
|
81
|
+
"""Stripe webhook event data."""
|
82
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
83
|
+
|
84
|
+
id: str = Field(..., description="Event ID")
|
85
|
+
object: str = Field(..., description="Object type")
|
86
|
+
api_version: str = Field(..., description="API version")
|
87
|
+
created: int = Field(..., description="Creation timestamp")
|
88
|
+
type: str = Field(..., description="Event type")
|
89
|
+
data: Dict[str, Any] = Field(..., description="Event data")
|
90
|
+
livemode: bool = Field(..., description="Live mode flag")
|
91
|
+
pending_webhooks: int = Field(..., description="Pending webhooks count")
|
92
|
+
request: Optional[Dict[str, Any]] = Field(None, description="Request info")
|
93
|
+
|
94
|
+
|
95
|
+
class StripeCharge(BaseModel):
|
96
|
+
"""Stripe charge object."""
|
97
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
98
|
+
|
99
|
+
id: str = Field(..., description="Charge ID")
|
100
|
+
object: str = Field(..., description="Object type")
|
101
|
+
amount: int = Field(..., description="Amount charged")
|
102
|
+
currency: str = Field(..., description="Currency code")
|
103
|
+
status: str = Field(..., description="Charge status")
|
104
|
+
created: int = Field(..., description="Creation timestamp")
|
105
|
+
description: Optional[str] = Field(None, description="Charge description")
|
106
|
+
metadata: Dict[str, str] = Field(default_factory=dict, description="Metadata")
|
107
|
+
payment_intent: Optional[str] = Field(None, description="PaymentIntent ID")
|
108
|
+
receipt_email: Optional[str] = Field(None, description="Receipt email")
|
109
|
+
receipt_url: Optional[str] = Field(None, description="Receipt URL")
|
110
|
+
|
111
|
+
|
112
|
+
class StripeCustomer(BaseModel):
|
113
|
+
"""Stripe customer object."""
|
114
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
115
|
+
|
116
|
+
id: str = Field(..., description="Customer ID")
|
117
|
+
object: str = Field(..., description="Object type")
|
118
|
+
created: int = Field(..., description="Creation timestamp")
|
119
|
+
email: Optional[str] = Field(None, description="Customer email")
|
120
|
+
name: Optional[str] = Field(None, description="Customer name")
|
121
|
+
phone: Optional[str] = Field(None, description="Customer phone")
|
122
|
+
metadata: Dict[str, str] = Field(default_factory=dict, description="Metadata")
|
123
|
+
|
124
|
+
|
125
|
+
class StripeError(BaseModel):
|
126
|
+
"""Stripe error response."""
|
127
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
128
|
+
|
129
|
+
type: str = Field(..., description="Error type")
|
130
|
+
code: Optional[str] = Field(None, description="Error code")
|
131
|
+
message: str = Field(..., description="Error message")
|
132
|
+
param: Optional[str] = Field(None, description="Parameter causing error")
|
133
|
+
decline_code: Optional[str] = Field(None, description="Decline code")
|
134
|
+
|
135
|
+
|
136
|
+
class StripeErrorResponse(BaseModel):
|
137
|
+
"""Stripe error response wrapper."""
|
138
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
139
|
+
|
140
|
+
error: StripeError = Field(..., description="Error details")
|
141
|
+
|
142
|
+
|
143
|
+
class StripeCurrenciesResponse(BaseModel):
|
144
|
+
"""Stripe supported currencies response."""
|
145
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
146
|
+
|
147
|
+
currencies: List[StripeCurrency] = Field(..., description="List of supported currencies")
|
148
|
+
|
149
|
+
|
150
|
+
class StripeWebhookEndpoint(BaseModel):
|
151
|
+
"""Stripe webhook endpoint configuration."""
|
152
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
153
|
+
|
154
|
+
id: str = Field(..., description="Webhook endpoint ID")
|
155
|
+
object: str = Field(..., description="Object type")
|
156
|
+
api_version: Optional[str] = Field(None, description="API version")
|
157
|
+
created: int = Field(..., description="Creation timestamp")
|
158
|
+
enabled_events: List[str] = Field(..., description="Enabled event types")
|
159
|
+
livemode: bool = Field(..., description="Live mode flag")
|
160
|
+
status: str = Field(..., description="Endpoint status")
|
161
|
+
url: str = Field(..., description="Endpoint URL")
|
162
|
+
|
163
|
+
|
164
|
+
# =============================================================================
|
165
|
+
# MONITORING & HEALTH CHECK MODELS
|
166
|
+
# =============================================================================
|
167
|
+
|
168
|
+
class StripeHealthErrorResponse(BaseModel):
|
169
|
+
"""Stripe API error response schema for health checks."""
|
170
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
171
|
+
|
172
|
+
class StripeHealthError(BaseModel):
|
173
|
+
message: str = Field(..., description="Error message")
|
174
|
+
type: str = Field(..., description="Error type")
|
175
|
+
|
176
|
+
error: StripeHealthError = Field(..., description="Error details")
|
177
|
+
|
178
|
+
@field_validator('error')
|
179
|
+
@classmethod
|
180
|
+
def validate_auth_error(cls, v):
|
181
|
+
"""Validate this is an authentication error (meaning API is healthy)."""
|
182
|
+
if v.type != 'invalid_request_error':
|
183
|
+
raise ValueError(f"Expected auth error, got '{v.type}'")
|
184
|
+
return v
|
@@ -0,0 +1,109 @@
|
|
1
|
+
from typing import Dict, Any, Optional, List
|
2
|
+
from decimal import Decimal
|
3
|
+
|
4
|
+
from ..base import PaymentProvider
|
5
|
+
from ...internal_types import ProviderResponse, WebhookData
|
6
|
+
from .models import StripeConfig, StripeCurrency, StripeNetwork
|
7
|
+
from django_cfg.modules.django_logger import get_logger
|
8
|
+
|
9
|
+
logger = get_logger("stripe")
|
10
|
+
|
11
|
+
|
12
|
+
class StripeProvider(PaymentProvider):
|
13
|
+
"""Stripe payment provider implementation."""
|
14
|
+
|
15
|
+
name = "stripe"
|
16
|
+
|
17
|
+
def __init__(self, config: StripeConfig):
|
18
|
+
super().__init__(config)
|
19
|
+
self.config = config
|
20
|
+
self.api_key = config.api_key
|
21
|
+
self.webhook_secret = config.webhook_secret
|
22
|
+
self.base_url = "https://api.stripe.com/v1"
|
23
|
+
|
24
|
+
def create_payment(self, amount_usd: Decimal, currency_code: str, description: str = None, **kwargs) -> ProviderResponse:
|
25
|
+
"""Create a payment intent with Stripe."""
|
26
|
+
# TODO: Implement Stripe payment creation
|
27
|
+
return ProviderResponse(
|
28
|
+
success=False,
|
29
|
+
error_message="Stripe provider not implemented yet"
|
30
|
+
)
|
31
|
+
|
32
|
+
def check_payment_status(self, payment_id: str) -> ProviderResponse:
|
33
|
+
"""Check payment status from Stripe."""
|
34
|
+
# TODO: Implement Stripe payment status check
|
35
|
+
return ProviderResponse(
|
36
|
+
success=False,
|
37
|
+
error_message="Stripe provider not implemented yet"
|
38
|
+
)
|
39
|
+
|
40
|
+
def process_webhook(self, webhook_data: WebhookData) -> ProviderResponse:
|
41
|
+
"""Process Stripe webhook."""
|
42
|
+
# TODO: Implement Stripe webhook processing
|
43
|
+
return ProviderResponse(
|
44
|
+
success=False,
|
45
|
+
error_message="Stripe provider not implemented yet"
|
46
|
+
)
|
47
|
+
|
48
|
+
def validate_webhook_signature(self, payload: str, signature: str) -> bool:
|
49
|
+
"""Validate Stripe webhook signature."""
|
50
|
+
# TODO: Implement Stripe signature validation
|
51
|
+
return False
|
52
|
+
|
53
|
+
def get_supported_currencies(self) -> ProviderResponse:
|
54
|
+
"""Get supported currencies from Stripe."""
|
55
|
+
# Common fiat currencies supported by Stripe
|
56
|
+
currencies = [
|
57
|
+
StripeCurrency(
|
58
|
+
currency_code='USD',
|
59
|
+
name='US Dollar',
|
60
|
+
decimal_digits=2,
|
61
|
+
min_amount=Decimal('0.50'),
|
62
|
+
is_zero_decimal=False
|
63
|
+
),
|
64
|
+
StripeCurrency(
|
65
|
+
currency_code='EUR',
|
66
|
+
name='Euro',
|
67
|
+
decimal_digits=2,
|
68
|
+
min_amount=Decimal('0.50'),
|
69
|
+
is_zero_decimal=False
|
70
|
+
),
|
71
|
+
StripeCurrency(
|
72
|
+
currency_code='GBP',
|
73
|
+
name='British Pound',
|
74
|
+
decimal_digits=2,
|
75
|
+
min_amount=Decimal('0.30'),
|
76
|
+
is_zero_decimal=False
|
77
|
+
),
|
78
|
+
StripeCurrency(
|
79
|
+
currency_code='JPY',
|
80
|
+
name='Japanese Yen',
|
81
|
+
decimal_digits=0,
|
82
|
+
min_amount=Decimal('50'),
|
83
|
+
is_zero_decimal=True
|
84
|
+
),
|
85
|
+
]
|
86
|
+
|
87
|
+
return ProviderResponse(
|
88
|
+
success=True,
|
89
|
+
data={'currencies': [c.model_dump() for c in currencies]}
|
90
|
+
)
|
91
|
+
|
92
|
+
def get_supported_networks(self, currency_code: str = None) -> ProviderResponse:
|
93
|
+
"""Get supported networks (not applicable for Stripe fiat payments)."""
|
94
|
+
return ProviderResponse(
|
95
|
+
success=True,
|
96
|
+
data={'networks': {}}
|
97
|
+
)
|
98
|
+
|
99
|
+
def get_currency_network_mapping(self) -> Dict[str, List[str]]:
|
100
|
+
"""Get currency network mapping (not applicable for Stripe)."""
|
101
|
+
return {}
|
102
|
+
|
103
|
+
def check_api_status(self) -> Dict[str, Any]:
|
104
|
+
"""Check Stripe API status."""
|
105
|
+
# TODO: Implement actual API health check
|
106
|
+
return {
|
107
|
+
'status': 'unknown',
|
108
|
+
'message': 'Stripe provider not implemented yet'
|
109
|
+
}
|
@@ -4,21 +4,22 @@ Critical Foundation Security Component.
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
import json
|
7
|
-
import
|
7
|
+
from django_cfg.modules.django_logger import get_logger
|
8
8
|
import traceback
|
9
9
|
from typing import Dict, Any, Optional, Union, Type
|
10
10
|
from enum import Enum
|
11
|
-
from datetime import datetime
|
11
|
+
from datetime import datetime, timedelta
|
12
12
|
from pydantic import BaseModel, Field
|
13
13
|
from django.http import JsonResponse, HttpResponse
|
14
14
|
from django.utils import timezone
|
15
15
|
from django.conf import settings
|
16
|
-
from .payment_notifications import payment_notifications
|
17
16
|
from django.core.cache import cache
|
17
|
+
from django.db.models import Count
|
18
18
|
|
19
|
-
from
|
19
|
+
from .payment_notifications import payment_notifications
|
20
|
+
from ...models.events import PaymentEvent
|
20
21
|
|
21
|
-
logger =
|
22
|
+
logger = get_logger("error_handler")
|
22
23
|
|
23
24
|
|
24
25
|
class ErrorSeverity(Enum):
|
@@ -593,9 +594,6 @@ class CentralizedErrorHandler:
|
|
593
594
|
def get_error_statistics(self, hours: int = 24) -> Dict[str, Any]:
|
594
595
|
"""Get error statistics for monitoring dashboard."""
|
595
596
|
|
596
|
-
from datetime import timedelta
|
597
|
-
from django.db.models import Count
|
598
|
-
|
599
597
|
since = timezone.now() - timedelta(hours=hours)
|
600
598
|
|
601
599
|
# Get error events from database
|