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.
Files changed (126) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin/__init__.py +3 -2
  3. django_cfg/apps/payments/admin/balance_admin.py +18 -18
  4. django_cfg/apps/payments/admin/currencies_admin.py +319 -131
  5. django_cfg/apps/payments/admin/payments_admin.py +15 -4
  6. django_cfg/apps/payments/config/module.py +2 -2
  7. django_cfg/apps/payments/config/utils.py +2 -2
  8. django_cfg/apps/payments/decorators.py +2 -2
  9. django_cfg/apps/payments/management/commands/README.md +95 -127
  10. django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
  11. django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
  12. django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
  13. django_cfg/apps/payments/managers/__init__.py +3 -2
  14. django_cfg/apps/payments/managers/balance_manager.py +2 -2
  15. django_cfg/apps/payments/managers/currency_manager.py +272 -49
  16. django_cfg/apps/payments/managers/payment_manager.py +161 -13
  17. django_cfg/apps/payments/middleware/api_access.py +2 -2
  18. django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
  19. django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
  20. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
  21. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
  22. django_cfg/apps/payments/models/__init__.py +3 -2
  23. django_cfg/apps/payments/models/currencies.py +187 -71
  24. django_cfg/apps/payments/models/payments.py +3 -2
  25. django_cfg/apps/payments/serializers/__init__.py +3 -2
  26. django_cfg/apps/payments/serializers/currencies.py +20 -12
  27. django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
  28. django_cfg/apps/payments/services/core/balance_service.py +2 -2
  29. django_cfg/apps/payments/services/core/fallback_service.py +2 -2
  30. django_cfg/apps/payments/services/core/payment_service.py +3 -6
  31. django_cfg/apps/payments/services/core/subscription_service.py +4 -7
  32. django_cfg/apps/payments/services/internal_types.py +171 -7
  33. django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
  34. django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
  35. django_cfg/apps/payments/services/providers/base.py +144 -43
  36. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
  37. django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
  38. django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
  39. django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
  40. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
  41. django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
  42. django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
  43. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
  44. django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
  45. django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
  46. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
  47. django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
  48. django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
  49. django_cfg/apps/payments/services/providers/registry.py +294 -11
  50. django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
  51. django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
  52. django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
  53. django_cfg/apps/payments/services/security/error_handler.py +6 -8
  54. django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
  55. django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
  56. django_cfg/apps/payments/signals/api_key_signals.py +2 -2
  57. django_cfg/apps/payments/signals/payment_signals.py +11 -5
  58. django_cfg/apps/payments/signals/subscription_signals.py +2 -2
  59. django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
  60. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
  61. django_cfg/apps/payments/templates/payments/base.html +4 -4
  62. django_cfg/apps/payments/templates/payments/components/payment_card.html +6 -6
  63. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +4 -4
  64. django_cfg/apps/payments/templates/payments/components/progress_bar.html +14 -7
  65. django_cfg/apps/payments/templates/payments/components/provider_stats.html +2 -2
  66. django_cfg/apps/payments/templates/payments/components/status_badge.html +8 -1
  67. django_cfg/apps/payments/templates/payments/components/status_overview.html +34 -30
  68. django_cfg/apps/payments/templates/payments/dashboard.html +202 -290
  69. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
  70. django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
  71. django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
  72. django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
  73. django_cfg/apps/payments/templates/payments/stats.html +261 -0
  74. django_cfg/apps/payments/templates/payments/test.html +213 -0
  75. django_cfg/apps/payments/urls.py +3 -1
  76. django_cfg/apps/payments/{urls_templates.py → urls_admin.py} +6 -0
  77. django_cfg/apps/payments/utils/__init__.py +1 -3
  78. django_cfg/apps/payments/utils/billing_utils.py +2 -2
  79. django_cfg/apps/payments/utils/config_utils.py +2 -8
  80. django_cfg/apps/payments/utils/validation_utils.py +2 -2
  81. django_cfg/apps/payments/views/__init__.py +3 -2
  82. django_cfg/apps/payments/views/currency_views.py +31 -20
  83. django_cfg/apps/payments/views/payment_views.py +2 -2
  84. django_cfg/apps/payments/views/templates/ajax.py +141 -2
  85. django_cfg/apps/payments/views/templates/base.py +21 -13
  86. django_cfg/apps/payments/views/templates/payment_detail.py +1 -1
  87. django_cfg/apps/payments/views/templates/payment_management.py +34 -40
  88. django_cfg/apps/payments/views/templates/stats.py +8 -4
  89. django_cfg/apps/payments/views/webhook_views.py +2 -2
  90. django_cfg/apps/payments/viewsets.py +3 -2
  91. django_cfg/apps/tasks/urls.py +0 -2
  92. django_cfg/apps/tasks/urls_admin.py +14 -0
  93. django_cfg/apps/urls.py +4 -4
  94. django_cfg/core/config.py +35 -0
  95. django_cfg/models/payments.py +2 -8
  96. django_cfg/modules/django_currency/__init__.py +16 -11
  97. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  98. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  99. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  100. django_cfg/modules/django_currency/core/__init__.py +1 -7
  101. django_cfg/modules/django_currency/core/converter.py +18 -23
  102. django_cfg/modules/django_currency/core/models.py +122 -11
  103. django_cfg/modules/django_currency/database/__init__.py +4 -4
  104. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  105. django_cfg/modules/django_unfold/dashboard.py +7 -2
  106. django_cfg/template_archive/django_sample.zip +0 -0
  107. django_cfg/templates/admin/components/action_grid.html +9 -9
  108. django_cfg/templates/admin/components/metric_card.html +5 -5
  109. django_cfg/templates/admin/components/status_badge.html +2 -2
  110. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  111. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  112. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  113. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  114. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/METADATA +2 -4
  115. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/RECORD +118 -96
  116. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  117. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  118. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  119. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  120. django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
  121. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  122. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  123. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  124. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
  125. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
  126. {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="forbid")
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
- sandbox: bool = False
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
- Pydantic schemas for provider API responses.
2
+ Provider API monitoring schemas.
3
3
 
4
- Type-safe models for validating and parsing responses
5
- from payment provider health check endpoints.
4
+ Re-exports provider-specific monitoring models from their dedicated folders.
5
+ Universal monitoring models and utilities.
6
6
  """
7
7
 
8
- from typing import Dict, Optional, Any
9
- from decimal import Decimal
10
- from datetime import datetime
11
- from pydantic import BaseModel, Field, validator
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
- class CryptAPIInfoResponse(BaseModel):
15
- """CryptAPI /btc/info/ response schema."""
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 StripeErrorResponse(BaseModel):
69
- """Stripe API error response schema."""
70
-
71
- class StripeError(BaseModel):
72
- message: str = Field(..., description="Error message")
73
- type: str = Field(..., description="Error type")
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 for unknown formats."""
100
-
101
- status_code: int = Field(..., description="HTTP status code")
102
- response_body: str = Field(..., description="Raw response body")
103
- response_time_ms: float = Field(..., description="Response time in milliseconds")
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
- """Unified health response model for all providers."""
118
-
119
- provider_name: str = Field(..., description="Provider name")
120
- is_healthy: bool = Field(..., description="Is provider healthy")
121
- status_code: int = Field(..., description="HTTP status code")
122
- response_time_ms: float = Field(..., description="Response time in milliseconds")
123
- error_message: Optional[str] = Field(None, description="Error message if unhealthy")
124
- parsed_response: Optional[Dict[str, Any]] = Field(None, description="Parsed API response")
125
- raw_response: Optional[str] = Field(None, description="Raw response body")
126
- checked_at: datetime = Field(default_factory=datetime.now, description="Check timestamp")
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
- class Config:
129
- json_encoders = {
130
- datetime: lambda v: v.isoformat()
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
- def parse_provider_response(provider_name: str, status_code: int, response_body: str, response_time_ms: float) -> ProviderHealthResponse:
135
- """
136
- Parse provider API response using appropriate schema.
137
-
138
- Args:
139
- provider_name: Name of the provider
140
- status_code: HTTP status code
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
- try:
152
- import json
153
- response_json = json.loads(response_body) if response_body else {}
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
- return ProviderHealthResponse(
215
- provider_name=provider_name,
216
- is_healthy=is_healthy,
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 logging
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 = logging.getLogger(__name__)
25
+ logger = get_logger("provider_health")
26
26
 
27
27
 
28
28
  class HealthStatus(Enum):