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
@@ -7,11 +7,137 @@ DO NOT duplicate Django ORM or DRF! Only for:
|
|
7
7
|
3. Configuration (settings and parameters)
|
8
8
|
"""
|
9
9
|
|
10
|
-
from pydantic import BaseModel, Field, ConfigDict
|
10
|
+
from pydantic import BaseModel, Field, ConfigDict, computed_field
|
11
11
|
from decimal import Decimal
|
12
12
|
from datetime import datetime
|
13
|
-
from typing import Optional, Dict, Any
|
13
|
+
from typing import Optional, Dict, Any, List
|
14
14
|
from enum import Enum
|
15
|
+
from django_cfg.modules.django_logger import get_logger
|
16
|
+
|
17
|
+
logger = get_logger("internal_types")
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
# =============================================================================
|
23
|
+
# UNIVERSAL CURRENCY MODEL - for provider → base communication
|
24
|
+
# =============================================================================
|
25
|
+
|
26
|
+
class UniversalCurrency(BaseModel):
|
27
|
+
"""Universal currency model that all providers should return."""
|
28
|
+
model_config = ConfigDict(validate_assignment=True, extra="allow")
|
29
|
+
|
30
|
+
# Core identification
|
31
|
+
provider_currency_code: str = Field(..., description="Original provider code: USDTERC20, USDTBSC, BTC")
|
32
|
+
base_currency_code: str = Field(..., description="Parsed base currency: USDT, BTC")
|
33
|
+
network_code: Optional[str] = Field(None, description="Parsed network: ethereum, bsc, bitcoin")
|
34
|
+
|
35
|
+
# Display info
|
36
|
+
name: str = Field(..., description="Human readable name")
|
37
|
+
currency_type: str = Field(default="crypto", description="fiat or crypto")
|
38
|
+
|
39
|
+
# Provider flags
|
40
|
+
is_enabled: bool = Field(default=True, description="Available for use")
|
41
|
+
is_popular: bool = Field(default=False, description="Popular currency")
|
42
|
+
is_stable: bool = Field(default=False, description="Stablecoin")
|
43
|
+
priority: int = Field(default=0, description="Display priority")
|
44
|
+
|
45
|
+
# URLs and assets
|
46
|
+
logo_url: str = Field(default="", description="Logo URL")
|
47
|
+
|
48
|
+
# Limits and availability
|
49
|
+
available_for_payment: bool = Field(default=True, description="Can receive payments")
|
50
|
+
available_for_payout: bool = Field(default=True, description="Can send payouts")
|
51
|
+
min_amount: Optional[float] = Field(None, description="Minimum amount")
|
52
|
+
max_amount: Optional[float] = Field(None, description="Maximum amount")
|
53
|
+
|
54
|
+
# Raw provider data
|
55
|
+
raw_data: Dict[str, Any] = Field(default_factory=dict, description="Original provider response")
|
56
|
+
|
57
|
+
|
58
|
+
class UniversalCurrenciesResponse(BaseModel):
|
59
|
+
"""Universal response with parsed currencies."""
|
60
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
61
|
+
|
62
|
+
currencies: List[UniversalCurrency] = Field(..., description="Parsed currencies")
|
63
|
+
|
64
|
+
|
65
|
+
# =============================================================================
|
66
|
+
# SYNCHRONIZATION RESULTS - Typed sync operation results
|
67
|
+
# =============================================================================
|
68
|
+
|
69
|
+
class ProviderSyncResult(BaseModel):
|
70
|
+
"""Result of provider synchronization operation."""
|
71
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
72
|
+
|
73
|
+
# Currencies operations
|
74
|
+
currencies_created: int = Field(default=0, description="Number of new currencies created")
|
75
|
+
currencies_updated: int = Field(default=0, description="Number of existing currencies updated")
|
76
|
+
|
77
|
+
# Networks operations
|
78
|
+
networks_created: int = Field(default=0, description="Number of new networks created")
|
79
|
+
networks_updated: int = Field(default=0, description="Number of existing networks updated")
|
80
|
+
|
81
|
+
# Provider currencies operations
|
82
|
+
provider_currencies_created: int = Field(default=0, description="Number of new provider currencies created")
|
83
|
+
provider_currencies_updated: int = Field(default=0, description="Number of existing provider currencies updated")
|
84
|
+
|
85
|
+
# Error tracking
|
86
|
+
errors: List[str] = Field(default_factory=list, description="List of errors encountered during sync")
|
87
|
+
|
88
|
+
@property
|
89
|
+
def total_items_processed(self) -> int:
|
90
|
+
"""Get total number of items processed."""
|
91
|
+
return (
|
92
|
+
self.currencies_created + self.currencies_updated +
|
93
|
+
self.networks_created + self.networks_updated +
|
94
|
+
self.provider_currencies_created + self.provider_currencies_updated
|
95
|
+
)
|
96
|
+
|
97
|
+
@property
|
98
|
+
def success(self) -> bool:
|
99
|
+
"""Check if sync completed without errors."""
|
100
|
+
return len(self.errors) == 0
|
101
|
+
|
102
|
+
@property
|
103
|
+
def has_changes(self) -> bool:
|
104
|
+
"""Check if any changes were made."""
|
105
|
+
return self.total_items_processed > 0
|
106
|
+
|
107
|
+
|
108
|
+
# AJAX Response Types
|
109
|
+
class CurrencyOptionModel(BaseModel):
|
110
|
+
"""Single currency option for UI select dropdown."""
|
111
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
112
|
+
|
113
|
+
provider_currency_code: str = Field(..., description="Provider-specific currency code")
|
114
|
+
display_name: str = Field(..., description="Human-readable display name")
|
115
|
+
base_currency_code: str = Field(..., description="Normalized base currency code")
|
116
|
+
base_currency_name: str = Field(..., description="Base currency full name")
|
117
|
+
network_code: Optional[str] = Field(None, description="Network code if applicable")
|
118
|
+
network_name: Optional[str] = Field(None, description="Network full name if applicable")
|
119
|
+
currency_type: str = Field(..., description="Currency type: crypto or fiat")
|
120
|
+
is_popular: bool = Field(default=False, description="Is this a popular currency")
|
121
|
+
is_stable: bool = Field(default=False, description="Is this a stablecoin")
|
122
|
+
available_for_payment: bool = Field(default=True, description="Available for payments")
|
123
|
+
available_for_payout: bool = Field(default=True, description="Available for payouts")
|
124
|
+
min_amount: Optional[str] = Field(None, description="Minimum amount as string")
|
125
|
+
max_amount: Optional[str] = Field(None, description="Maximum amount as string")
|
126
|
+
logo_url: Optional[str] = Field(None, description="Currency logo URL")
|
127
|
+
# Exchange rates
|
128
|
+
usd_rate: float = Field(default=0.0, description="1 CURRENCY = X USD")
|
129
|
+
tokens_per_usd: float = Field(default=0.0, description="How many tokens for 1 USD")
|
130
|
+
|
131
|
+
|
132
|
+
class ProviderCurrencyOptionsResponse(BaseModel):
|
133
|
+
"""Response for provider currency options API."""
|
134
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
135
|
+
|
136
|
+
success: bool = Field(..., description="API call success status")
|
137
|
+
provider: str = Field(..., description="Provider name")
|
138
|
+
currency_options: List[CurrencyOptionModel] = Field(default_factory=list, description="Available currency options")
|
139
|
+
count: int = Field(..., description="Number of currency options")
|
140
|
+
error: Optional[str] = Field(None, description="Error message if any")
|
15
141
|
|
16
142
|
|
17
143
|
# =============================================================================
|
@@ -30,6 +156,8 @@ class ProviderResponse(BaseModel):
|
|
30
156
|
pay_address: Optional[str] = None
|
31
157
|
status: Optional[str] = None
|
32
158
|
error_message: Optional[str] = None
|
159
|
+
data: Dict[str, Any] = Field(default_factory=dict)
|
160
|
+
|
33
161
|
# Legacy fields for backward compatibility with tests
|
34
162
|
amount: Optional[Decimal] = None
|
35
163
|
currency: Optional[str] = None
|
@@ -38,6 +166,20 @@ class ProviderResponse(BaseModel):
|
|
38
166
|
currency_code: Optional[str] = None
|
39
167
|
|
40
168
|
|
169
|
+
class PaymentAmountEstimate(BaseModel):
|
170
|
+
"""Universal payment amount estimation response"""
|
171
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
172
|
+
|
173
|
+
currency_from: str = Field(description="Source currency code")
|
174
|
+
currency_to: str = Field(description="Target currency code")
|
175
|
+
amount_from: Decimal = Field(gt=0, description="Source amount")
|
176
|
+
estimated_amount: Decimal = Field(gt=0, description="Estimated target amount")
|
177
|
+
fee_amount: Optional[Decimal] = Field(None, ge=0, description="Provider fee amount")
|
178
|
+
exchange_rate: Optional[Decimal] = Field(None, gt=0, description="Exchange rate used")
|
179
|
+
provider_name: str = Field(description="Provider that made the estimation")
|
180
|
+
estimated_at: Optional[datetime] = Field(None, description="When estimation was made")
|
181
|
+
|
182
|
+
|
41
183
|
class WebhookData(BaseModel):
|
42
184
|
"""Provider webhook validation"""
|
43
185
|
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
@@ -114,14 +256,36 @@ class RedisConfig(BaseModel):
|
|
114
256
|
|
115
257
|
|
116
258
|
class ProviderConfig(BaseModel):
|
117
|
-
"""Base provider configuration"""
|
118
|
-
model_config = ConfigDict(validate_assignment=True, extra="
|
259
|
+
"""Base provider configuration with automatic sandbox detection"""
|
260
|
+
model_config = ConfigDict(validate_assignment=True, extra="allow") # Allow extra fields for flexibility
|
119
261
|
|
120
262
|
enabled: bool = True
|
121
263
|
api_key: str
|
122
|
-
|
123
|
-
timeout_seconds: int = 30
|
264
|
+
timeout_seconds: int = Field(default=30, alias='timeout', description="Request timeout in seconds")
|
124
265
|
max_retries: int = 3
|
266
|
+
|
267
|
+
@computed_field
|
268
|
+
@property
|
269
|
+
def sandbox(self) -> bool:
|
270
|
+
"""Get sandbox mode from django-cfg config."""
|
271
|
+
try:
|
272
|
+
from django_cfg.core.config import get_current_config
|
273
|
+
current_config = get_current_config()
|
274
|
+
|
275
|
+
if current_config:
|
276
|
+
# Check env_mode first
|
277
|
+
if hasattr(current_config, 'env_mode'):
|
278
|
+
env_mode = current_config.env_mode
|
279
|
+
if isinstance(env_mode, str):
|
280
|
+
return env_mode.lower() in ['development', 'dev', 'test']
|
281
|
+
|
282
|
+
# Fallback to debug flag
|
283
|
+
if hasattr(current_config, 'debug'):
|
284
|
+
return current_config.debug
|
285
|
+
|
286
|
+
return True # Default to sandbox for safety
|
287
|
+
except Exception:
|
288
|
+
return True
|
125
289
|
|
126
290
|
|
127
291
|
# =============================================================================
|
@@ -294,4 +458,4 @@ class ProviderInfo(BaseModel):
|
|
294
458
|
display_name: str
|
295
459
|
supported_currencies: list[str] = Field(default_factory=list)
|
296
460
|
is_active: bool
|
297
|
-
features: Dict[str, Any] = Field(default_factory=dict)
|
461
|
+
features: Dict[str, Any] = Field(default_factory=dict)
|
@@ -1,222 +1,76 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
Provider API monitoring schemas.
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
Re-exports provider-specific monitoring models from their dedicated folders.
|
5
|
+
Universal monitoring models and utilities.
|
6
6
|
"""
|
7
7
|
|
8
|
-
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from
|
8
|
+
# Re-export provider-specific health check models
|
9
|
+
from ..providers.cryptapi.models import CryptAPIInfoResponse
|
10
|
+
from ..providers.nowpayments.models import NowPaymentsStatusResponse
|
11
|
+
from ..providers.cryptomus.models import CryptomusErrorResponse
|
12
|
+
from ..providers.stripe.models import StripeHealthErrorResponse
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
coin: str = Field(..., description="Cryptocurrency name")
|
18
|
-
logo: str = Field(..., description="Logo URL")
|
19
|
-
ticker: str = Field(..., description="Currency ticker")
|
20
|
-
minimum_transaction: int = Field(..., description="Minimum transaction in satoshis")
|
21
|
-
minimum_transaction_coin: str = Field(..., description="Minimum transaction in coin units")
|
22
|
-
minimum_fee: int = Field(..., description="Minimum fee in satoshis")
|
23
|
-
minimum_fee_coin: str = Field(..., description="Minimum fee in coin units")
|
24
|
-
fee_percent: str = Field(..., description="Fee percentage")
|
25
|
-
network_fee_estimation: str = Field(..., description="Network fee estimation")
|
26
|
-
status: str = Field(..., description="API status")
|
27
|
-
prices: Dict[str, str] = Field(..., description="Prices in various fiat currencies")
|
28
|
-
prices_updated: str = Field(..., description="Prices last updated timestamp")
|
29
|
-
|
30
|
-
@validator('status')
|
31
|
-
def validate_status(cls, v):
|
32
|
-
"""Validate that status is success."""
|
33
|
-
if v != 'success':
|
34
|
-
raise ValueError(f"Expected status 'success', got '{v}'")
|
35
|
-
return v
|
36
|
-
|
37
|
-
@validator('prices')
|
38
|
-
def validate_prices_not_empty(cls, v):
|
39
|
-
"""Validate that prices dict is not empty."""
|
40
|
-
if not v:
|
41
|
-
raise ValueError("Prices dictionary cannot be empty")
|
42
|
-
return v
|
43
|
-
|
44
|
-
def get_usd_price(self) -> Optional[Decimal]:
|
45
|
-
"""Get USD price as Decimal."""
|
46
|
-
usd_price = self.prices.get('USD')
|
47
|
-
if usd_price:
|
48
|
-
try:
|
49
|
-
return Decimal(usd_price)
|
50
|
-
except:
|
51
|
-
return None
|
52
|
-
return None
|
53
|
-
|
54
|
-
|
55
|
-
class NowPaymentsStatusResponse(BaseModel):
|
56
|
-
"""NowPayments /v1/status response schema."""
|
57
|
-
|
58
|
-
message: str = Field(..., description="Status message")
|
59
|
-
|
60
|
-
@validator('message')
|
61
|
-
def validate_message_ok(cls, v):
|
62
|
-
"""Validate that message is OK."""
|
63
|
-
if v.upper() != 'OK':
|
64
|
-
raise ValueError(f"Expected message 'OK', got '{v}'")
|
65
|
-
return v
|
14
|
+
# Universal monitoring models - defined here
|
15
|
+
from pydantic import BaseModel, Field
|
16
|
+
from typing import Dict, Any, Optional
|
17
|
+
from enum import Enum
|
66
18
|
|
67
19
|
|
68
|
-
class
|
69
|
-
"""
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
error: StripeError = Field(..., description="Error details")
|
76
|
-
|
77
|
-
@validator('error')
|
78
|
-
def validate_auth_error(cls, v):
|
79
|
-
"""Validate this is an authentication error (meaning API is healthy)."""
|
80
|
-
if v.type != 'invalid_request_error':
|
81
|
-
raise ValueError(f"Expected auth error, got '{v.type}'")
|
82
|
-
return v
|
83
|
-
|
84
|
-
|
85
|
-
class CryptomusErrorResponse(BaseModel):
|
86
|
-
"""Cryptomus API error response schema."""
|
87
|
-
|
88
|
-
error: str = Field(..., description="Error message")
|
89
|
-
|
90
|
-
@validator('error')
|
91
|
-
def validate_not_found_error(cls, v):
|
92
|
-
"""Validate this is a not found error (meaning API is responding)."""
|
93
|
-
if v.lower() not in ['not found', 'unauthorized', 'forbidden']:
|
94
|
-
raise ValueError(f"Unexpected error: {v}")
|
95
|
-
return v
|
20
|
+
class APIHealthStatus(str, Enum):
|
21
|
+
"""API health status enumeration."""
|
22
|
+
HEALTHY = "healthy"
|
23
|
+
DEGRADED = "degraded"
|
24
|
+
UNHEALTHY = "unhealthy"
|
25
|
+
UNKNOWN = "unknown"
|
96
26
|
|
97
27
|
|
98
28
|
class GenericAPIHealthResponse(BaseModel):
|
99
|
-
"""Generic API health response
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
def is_healthy(self) -> bool:
|
106
|
-
"""Determine if API is healthy based on status code."""
|
107
|
-
# 2xx = healthy, 401/403 = healthy (auth required), 4xx = degraded, 5xx = unhealthy
|
108
|
-
if 200 <= self.status_code < 300:
|
109
|
-
return True
|
110
|
-
elif self.status_code in [401, 403]:
|
111
|
-
return True # Auth required but API responding
|
112
|
-
else:
|
113
|
-
return False
|
29
|
+
"""Generic API health check response."""
|
30
|
+
status: APIHealthStatus = Field(description="API health status")
|
31
|
+
response_time_ms: float = Field(description="Response time in milliseconds")
|
32
|
+
error_message: Optional[str] = Field(None, description="Error message if unhealthy")
|
33
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
114
34
|
|
115
35
|
|
116
36
|
class ProviderHealthResponse(BaseModel):
|
117
|
-
"""
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
37
|
+
"""Provider health check response wrapper."""
|
38
|
+
provider_name: str = Field(description="Provider name")
|
39
|
+
api_health: GenericAPIHealthResponse = Field(description="API health details")
|
40
|
+
checked_at: str = Field(description="Check timestamp")
|
41
|
+
|
42
|
+
|
43
|
+
def parse_provider_response(provider_name: str, response_data: Dict[str, Any]) -> ProviderHealthResponse:
|
44
|
+
"""Parse provider API response into standardized health format."""
|
45
|
+
# Simple implementation - can be enhanced per provider
|
46
|
+
status = APIHealthStatus.HEALTHY if response_data.get('success', False) else APIHealthStatus.UNHEALTHY
|
47
|
+
|
48
|
+
api_health = GenericAPIHealthResponse(
|
49
|
+
status=status,
|
50
|
+
response_time_ms=response_data.get('response_time', 0.0),
|
51
|
+
error_message=response_data.get('error_message'),
|
52
|
+
metadata=response_data.get('metadata', {})
|
53
|
+
)
|
127
54
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
55
|
+
return ProviderHealthResponse(
|
56
|
+
provider_name=provider_name,
|
57
|
+
api_health=api_health,
|
58
|
+
checked_at=response_data.get('timestamp', '')
|
59
|
+
)
|
132
60
|
|
133
61
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
response_body: Raw response body
|
142
|
-
response_time_ms: Response time in milliseconds
|
143
|
-
|
144
|
-
Returns:
|
145
|
-
ProviderHealthResponse with parsed data
|
146
|
-
"""
|
147
|
-
parsed_response = None
|
148
|
-
error_message = None
|
149
|
-
is_healthy = False
|
62
|
+
# Backward compatibility exports
|
63
|
+
__all__ = [
|
64
|
+
# Provider-specific models
|
65
|
+
'CryptAPIInfoResponse',
|
66
|
+
'NowPaymentsStatusResponse',
|
67
|
+
'CryptomusErrorResponse',
|
68
|
+
'StripeHealthErrorResponse',
|
150
69
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
if provider_name == 'cryptapi':
|
156
|
-
if status_code == 200:
|
157
|
-
cryptapi_response = CryptAPIInfoResponse(**response_json)
|
158
|
-
parsed_response = cryptapi_response.dict()
|
159
|
-
is_healthy = True
|
160
|
-
else:
|
161
|
-
error_message = f"CryptAPI returned status {status_code}"
|
162
|
-
|
163
|
-
elif provider_name == 'nowpayments':
|
164
|
-
if status_code == 200:
|
165
|
-
nowpayments_response = NowPaymentsStatusResponse(**response_json)
|
166
|
-
parsed_response = nowpayments_response.dict()
|
167
|
-
is_healthy = True
|
168
|
-
else:
|
169
|
-
error_message = f"NowPayments returned status {status_code}"
|
170
|
-
|
171
|
-
elif provider_name == 'stripe':
|
172
|
-
if status_code == 401:
|
173
|
-
stripe_response = StripeErrorResponse(**response_json)
|
174
|
-
parsed_response = stripe_response.dict()
|
175
|
-
is_healthy = True # Auth error = API responding
|
176
|
-
elif 200 <= status_code < 300:
|
177
|
-
parsed_response = response_json
|
178
|
-
is_healthy = True
|
179
|
-
else:
|
180
|
-
error_message = f"Stripe returned unexpected status {status_code}"
|
181
|
-
|
182
|
-
elif provider_name == 'cryptomus':
|
183
|
-
if status_code == 404 and response_json.get('error') == 'Not found':
|
184
|
-
cryptomus_response = CryptomusErrorResponse(**response_json)
|
185
|
-
parsed_response = cryptomus_response.dict()
|
186
|
-
is_healthy = True # Not found = API responding
|
187
|
-
elif status_code == 204:
|
188
|
-
# No Content = API responding and healthy
|
189
|
-
parsed_response = {'status': 'no_content', 'message': 'API responding correctly'}
|
190
|
-
is_healthy = True
|
191
|
-
elif status_code in [401, 403]:
|
192
|
-
is_healthy = True # Auth required = API responding
|
193
|
-
parsed_response = response_json
|
194
|
-
elif 200 <= status_code < 300:
|
195
|
-
parsed_response = response_json
|
196
|
-
is_healthy = True
|
197
|
-
else:
|
198
|
-
error_message = f"Cryptomus returned status {status_code}"
|
199
|
-
|
200
|
-
else:
|
201
|
-
# Generic handling for unknown providers
|
202
|
-
generic_response = GenericAPIHealthResponse(
|
203
|
-
status_code=status_code,
|
204
|
-
response_body=response_body,
|
205
|
-
response_time_ms=response_time_ms
|
206
|
-
)
|
207
|
-
parsed_response = generic_response.dict()
|
208
|
-
is_healthy = generic_response.is_healthy()
|
209
|
-
|
210
|
-
except Exception as e:
|
211
|
-
error_message = f"Failed to parse {provider_name} response: {str(e)}"
|
212
|
-
is_healthy = False
|
70
|
+
# Universal models
|
71
|
+
'GenericAPIHealthResponse',
|
72
|
+
'ProviderHealthResponse',
|
213
73
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
status_code=status_code,
|
218
|
-
response_time_ms=response_time_ms,
|
219
|
-
error_message=error_message,
|
220
|
-
parsed_response=parsed_response,
|
221
|
-
raw_response=response_body
|
222
|
-
)
|
74
|
+
# Utility functions
|
75
|
+
'parse_provider_response',
|
76
|
+
]
|
@@ -5,7 +5,7 @@ Monitors the health of all payment providers and provides
|
|
5
5
|
fallback mechanisms when providers are unavailable.
|
6
6
|
"""
|
7
7
|
|
8
|
-
import
|
8
|
+
from django_cfg.modules.django_logger import get_logger
|
9
9
|
import time
|
10
10
|
import asyncio
|
11
11
|
import requests
|
@@ -22,7 +22,7 @@ from ..providers.registry import ProviderRegistry
|
|
22
22
|
from ...models.events import PaymentEvent
|
23
23
|
from .api_schemas import parse_provider_response
|
24
24
|
|
25
|
-
logger =
|
25
|
+
logger = get_logger("provider_health")
|
26
26
|
|
27
27
|
|
28
28
|
class HealthStatus(Enum):
|