django-cfg 1.2.29__py3-none-any.whl → 1.3.1__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/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -9
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +600 -108
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +470 -64
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
- django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +16 -20
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +207 -67
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -284
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +344 -468
- django_cfg/apps/payments/services/core/subscription_service.py +425 -484
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +232 -71
- django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
- django_cfg/apps/payments/services/providers/registry.py +429 -80
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +211 -130
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +129 -98
- django_cfg/apps/payments/signals/subscription_signals.py +195 -143
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +46 -47
- django_cfg/apps/payments/urls_admin.py +49 -0
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- 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/config.py +1 -1
- django_cfg/core/config.py +75 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -498
- 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_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +65 -12
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -178
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
- 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/managers/__init__.py +0 -22
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -83
- django_cfg/apps/payments/managers/payment_manager.py +0 -44
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -56
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -55
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -297
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- 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/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -637
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/urls_templates.py +0 -52
- django_cfg/apps/payments/utils/__init__.py +0 -45
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -245
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -62
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -111
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -312
- django_cfg/apps/payments/views/templates/base.py +0 -204
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -164
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -240
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -65
- django_cfg/core/integration.py +0 -160
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,372 +0,0 @@
|
|
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
|
@@ -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)])
|