django-cfg 1.2.27__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 (138) 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/static/payments/css/payments.css +340 -0
  60. django_cfg/apps/payments/static/payments/js/notifications.js +202 -0
  61. django_cfg/apps/payments/static/payments/js/payment-utils.js +318 -0
  62. django_cfg/apps/payments/static/payments/js/theme.js +86 -0
  63. django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
  64. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
  65. django_cfg/apps/payments/templates/payments/base.html +182 -0
  66. django_cfg/apps/payments/templates/payments/components/payment_card.html +201 -0
  67. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +109 -0
  68. django_cfg/apps/payments/templates/payments/components/progress_bar.html +43 -0
  69. django_cfg/apps/payments/templates/payments/components/provider_stats.html +40 -0
  70. django_cfg/apps/payments/templates/payments/components/status_badge.html +34 -0
  71. django_cfg/apps/payments/templates/payments/components/status_overview.html +148 -0
  72. django_cfg/apps/payments/templates/payments/dashboard.html +258 -0
  73. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
  74. django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
  75. django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
  76. django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
  77. django_cfg/apps/payments/templates/payments/stats.html +261 -0
  78. django_cfg/apps/payments/templates/payments/test.html +213 -0
  79. django_cfg/apps/payments/templatetags/__init__.py +1 -0
  80. django_cfg/apps/payments/templatetags/payments_tags.py +315 -0
  81. django_cfg/apps/payments/urls.py +3 -1
  82. django_cfg/apps/payments/urls_admin.py +58 -0
  83. django_cfg/apps/payments/utils/__init__.py +1 -3
  84. django_cfg/apps/payments/utils/billing_utils.py +2 -2
  85. django_cfg/apps/payments/utils/config_utils.py +2 -8
  86. django_cfg/apps/payments/utils/validation_utils.py +2 -2
  87. django_cfg/apps/payments/views/__init__.py +3 -2
  88. django_cfg/apps/payments/views/currency_views.py +31 -20
  89. django_cfg/apps/payments/views/payment_views.py +2 -2
  90. django_cfg/apps/payments/views/templates/__init__.py +25 -0
  91. django_cfg/apps/payments/views/templates/ajax.py +451 -0
  92. django_cfg/apps/payments/views/templates/base.py +212 -0
  93. django_cfg/apps/payments/views/templates/dashboard.py +60 -0
  94. django_cfg/apps/payments/views/templates/payment_detail.py +102 -0
  95. django_cfg/apps/payments/views/templates/payment_management.py +158 -0
  96. django_cfg/apps/payments/views/templates/qr_code.py +174 -0
  97. django_cfg/apps/payments/views/templates/stats.py +244 -0
  98. django_cfg/apps/payments/views/templates/utils.py +181 -0
  99. django_cfg/apps/payments/views/webhook_views.py +2 -2
  100. django_cfg/apps/payments/viewsets.py +3 -2
  101. django_cfg/apps/tasks/urls.py +0 -2
  102. django_cfg/apps/tasks/urls_admin.py +14 -0
  103. django_cfg/apps/urls.py +6 -3
  104. django_cfg/core/config.py +35 -0
  105. django_cfg/models/payments.py +2 -8
  106. django_cfg/modules/django_currency/__init__.py +16 -11
  107. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  108. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  109. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  110. django_cfg/modules/django_currency/core/__init__.py +1 -7
  111. django_cfg/modules/django_currency/core/converter.py +18 -23
  112. django_cfg/modules/django_currency/core/models.py +122 -11
  113. django_cfg/modules/django_currency/database/__init__.py +4 -4
  114. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  115. django_cfg/modules/django_unfold/dashboard.py +7 -2
  116. django_cfg/registry/core.py +1 -0
  117. django_cfg/template_archive/.gitignore +1 -0
  118. django_cfg/template_archive/django_sample.zip +0 -0
  119. django_cfg/templates/admin/components/action_grid.html +9 -9
  120. django_cfg/templates/admin/components/metric_card.html +5 -5
  121. django_cfg/templates/admin/components/status_badge.html +2 -2
  122. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  123. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  124. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  125. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  126. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/METADATA +13 -18
  127. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/RECORD +130 -83
  128. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  129. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  130. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  131. django_cfg/apps/payments/services/providers/cryptomus.py +0 -310
  132. django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
  133. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  134. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  135. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  136. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
  137. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
  138. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -5,20 +5,39 @@ Abstract base class for all payment providers.
5
5
  """
6
6
 
7
7
  from abc import ABC, abstractmethod
8
- from typing import Optional, List
8
+ from typing import Optional, List, Dict
9
+ from django.db.models import QuerySet
9
10
  from decimal import Decimal
10
11
 
11
- from ..internal_types import ProviderResponse, WebhookData
12
+ from django.db import models, transaction
13
+ from cachetools import TTLCache
12
14
 
15
+ from ..internal_types import ProviderResponse, WebhookData, PaymentAmountEstimate, ProviderInfo, UniversalCurrency, UniversalCurrenciesResponse, ProviderSyncResult
16
+ from ...models import Currency, Network, ProviderCurrency
17
+ from django_cfg.modules.django_logger import get_logger
18
+
19
+
20
+ logger = get_logger('base_provider')
13
21
 
14
22
  class PaymentProvider(ABC):
15
23
  """Abstract base class for payment providers."""
16
24
 
17
- def __init__(self, config: dict):
25
+ # Class-level cache for all providers (5 min TTL)
26
+ _api_cache = TTLCache(maxsize=100, ttl=300)
27
+
28
+ def __init__(self, config):
18
29
  """Initialize provider with config."""
19
30
  self.config = config
20
31
  self.name = self.__class__.__name__.lower().replace('provider', '')
21
- self.enabled = config.get('enabled', True)
32
+ self.logger = get_logger(f"payment.{self.name}")
33
+
34
+ # Handle both dict and Pydantic model configs
35
+ if hasattr(config, 'enabled'):
36
+ self.enabled = config.enabled
37
+ elif hasattr(config, 'get'):
38
+ self.enabled = config.get('enabled', True)
39
+ else:
40
+ self.enabled = getattr(config, 'enabled', True)
22
41
 
23
42
  @abstractmethod
24
43
  def create_payment(self, payment_data: dict) -> ProviderResponse:
@@ -61,16 +80,23 @@ class PaymentProvider(ABC):
61
80
  """
62
81
  pass
63
82
 
64
- @abstractmethod
65
- def get_supported_currencies(self) -> List[str]:
83
+ @abstractmethod
84
+ def get_parsed_currencies(self) -> UniversalCurrenciesResponse:
66
85
  """
67
- Get list of supported currencies.
86
+ Get parsed and normalized currencies ready for database sync.
87
+
88
+ This method should:
89
+ 1. Fetch data from provider API
90
+ 2. Parse provider codes into base_currency + network
91
+ 3. Return universal format
68
92
 
69
93
  Returns:
70
- List of supported currency codes
94
+ UniversalCurrenciesResponse with parsed data
71
95
  """
72
96
  pass
73
97
 
98
+
99
+
74
100
  def validate_webhook(self, payload: dict, headers: Optional[dict] = None) -> bool:
75
101
  """
76
102
  Validate webhook signature and data.
@@ -85,33 +111,6 @@ class PaymentProvider(ABC):
85
111
  # Default implementation - providers can override
86
112
  return True
87
113
 
88
- def get_minimum_payment_amount(self, currency_from: str, currency_to: str = 'usd') -> Optional[Decimal]:
89
- """
90
- Get minimum payment amount for currency pair.
91
-
92
- Args:
93
- currency_from: Source currency
94
- currency_to: Target currency
95
-
96
- Returns:
97
- Minimum payment amount or None if not supported
98
- """
99
- # Optional method - providers can override
100
- return None
101
-
102
- def estimate_payment_amount(self, amount: Decimal, currency_code: str) -> Optional[dict]:
103
- """
104
- Estimate payment amount in target currency.
105
-
106
- Args:
107
- amount: Amount to estimate
108
- currency_code: Target currency
109
-
110
- Returns:
111
- Dict with estimation data or None if not supported
112
- """
113
- # Optional method - providers can override
114
- return None
115
114
 
116
115
  def check_api_status(self) -> bool:
117
116
  """
@@ -127,11 +126,113 @@ class PaymentProvider(ABC):
127
126
  """Check if provider is enabled."""
128
127
  return self.enabled
129
128
 
130
- def get_provider_info(self) -> dict:
131
- """Get provider information."""
132
- return {
133
- 'name': self.name,
134
- 'enabled': self.enabled,
135
- 'supported_currencies': self.get_supported_currencies(),
136
- 'api_status': self.check_api_status(),
137
- }
129
+ def sync_currencies_to_db(self) -> ProviderSyncResult:
130
+ """
131
+ Synchronize provider currencies with clean architecture.
132
+ Uses get_parsed_currencies() to get normalized data.
133
+ """
134
+ result = ProviderSyncResult()
135
+
136
+ # Get parsed data from provider
137
+ try:
138
+ parsed_response = self.get_parsed_currencies()
139
+ except Exception as e:
140
+ result.errors.append(f"Failed to get parsed currencies: {str(e)}")
141
+ return result
142
+
143
+ logger.info(f"Processing {len(parsed_response.currencies)} currencies from {self.name}")
144
+
145
+ with transaction.atomic():
146
+ for universal_currency in parsed_response.currencies:
147
+ try:
148
+ # 1. Create/update base Currency using normalized manager
149
+ currency, created = Currency.objects.get_or_create_normalized(
150
+ code=universal_currency.base_currency_code,
151
+ defaults={
152
+ 'name': universal_currency.name,
153
+ 'currency_type': universal_currency.currency_type
154
+ }
155
+ )
156
+
157
+ if created:
158
+ result.currencies_created += 1
159
+ logger.debug(f"Created currency: {universal_currency.base_currency_code}")
160
+ else:
161
+ result.currencies_updated += 1
162
+
163
+ # 2. Create/update Network (if needed) using normalized manager
164
+ network = None
165
+ if universal_currency.network_code:
166
+ network, created = Network.objects.get_or_create_normalized(
167
+ code=universal_currency.network_code,
168
+ defaults={
169
+ 'name': universal_currency.network_code.title()
170
+ }
171
+ )
172
+
173
+ if created:
174
+ result.networks_created += 1
175
+ logger.debug(f"Created network: {universal_currency.network_code}")
176
+ else:
177
+ result.networks_updated += 1
178
+
179
+ # 3. Create/update ProviderCurrency mapping
180
+ provider_currency, created = ProviderCurrency.objects.get_or_create(
181
+ provider_name=self.name,
182
+ provider_currency_code=universal_currency.provider_currency_code,
183
+ defaults={
184
+ 'base_currency': currency,
185
+ 'network': network,
186
+ 'is_enabled': universal_currency.is_enabled,
187
+ 'is_popular': universal_currency.is_popular,
188
+ 'is_stable': universal_currency.is_stable,
189
+ 'priority': universal_currency.priority,
190
+ 'logo_url': universal_currency.logo_url,
191
+ 'available_for_payment': universal_currency.available_for_payment,
192
+ 'available_for_payout': universal_currency.available_for_payout,
193
+ 'min_amount': universal_currency.min_amount,
194
+ 'max_amount': universal_currency.max_amount,
195
+ 'metadata': universal_currency.raw_data
196
+ }
197
+ )
198
+
199
+ if created:
200
+ result.provider_currencies_created += 1
201
+ logger.debug(f"Created provider currency: {universal_currency.provider_currency_code}")
202
+ else:
203
+ # Update existing record
204
+ provider_currency.is_enabled = universal_currency.is_enabled
205
+ provider_currency.is_popular = universal_currency.is_popular
206
+ provider_currency.is_stable = universal_currency.is_stable
207
+ provider_currency.priority = universal_currency.priority
208
+ provider_currency.logo_url = universal_currency.logo_url
209
+ provider_currency.save()
210
+ result.provider_currencies_updated += 1
211
+
212
+ except Exception as e:
213
+ error_msg = f"Error processing {universal_currency.provider_currency_code}: {str(e)}"
214
+ result.errors.append(error_msg)
215
+ logger.error(error_msg)
216
+
217
+ logger.info(f"Sync completed: {result}")
218
+ return result
219
+
220
+ def get_provider_info(self) -> ProviderInfo:
221
+ """Get provider information using parsed currencies."""
222
+ try:
223
+ parsed_response = self.get_parsed_currencies()
224
+ supported_currencies = [c.base_currency_code for c in parsed_response.currencies]
225
+ except Exception:
226
+ supported_currencies = []
227
+
228
+ return ProviderInfo(
229
+ name=self.name,
230
+ display_name=self.name.title(),
231
+ supported_currencies=supported_currencies,
232
+ is_active=self.enabled and self.check_api_status(),
233
+ features={
234
+ 'supports_networks': True,
235
+ 'supports_webhooks': True,
236
+ 'supports_refunds': True
237
+ }
238
+ )
@@ -0,0 +1,4 @@
1
+ from .provider import CryptAPIProvider
2
+ from .models import CryptAPIConfig, CryptAPICurrency, CryptAPINetwork
3
+
4
+ __all__ = ['CryptAPIProvider', 'CryptAPIConfig', 'CryptAPICurrency', 'CryptAPINetwork']
@@ -0,0 +1,8 @@
1
+
2
+
3
+ PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
4
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC3FT0Ym8b3myVxhQW7ESuuu6lo
5
+ dGAsUJs4fq+Ey//jm27jQ7HHHDmP1YJO7XE7Jf/0DTEJgcw4EZhJFVwsk6d3+4fy
6
+ Bsn0tKeyGMiaE6cVkX0cy6Y85o8zgc/CwZKc0uw6d5siAo++xl2zl+RGMXCELQVE
7
+ ox7pp208zTvown577wIDAQAB
8
+ -----END PUBLIC KEY-----"""
@@ -0,0 +1,192 @@
1
+ from pydantic import BaseModel, Field, ConfigDict, field_validator
2
+ from typing import Optional, List, Dict, Any
3
+ from decimal import Decimal
4
+
5
+ from ...internal_types import ProviderConfig
6
+ from .config import PUBLIC_KEY
7
+
8
+ class CryptAPIConfig(ProviderConfig):
9
+ """CryptAPI provider configuration with Pydantic v2."""
10
+
11
+ own_address: str = Field(..., description="Your cryptocurrency address")
12
+ callback_url: Optional[str] = Field(default=None, description="Webhook callback URL")
13
+ convert_payments: bool = Field(default=True, description="Auto-convert payments")
14
+ multi_token: bool = Field(default=True, description="Support multi-token payments")
15
+ priority: str = Field(default='default', description="Transaction priority")
16
+ verify_signatures: bool = Field(default=True, description="Enable webhook signature verification")
17
+
18
+ # CryptAPI's official public key for signature verification
19
+ public_key: str = Field(
20
+ default=PUBLIC_KEY,
21
+ description="CryptAPI RSA public key for signature verification"
22
+ )
23
+
24
+
25
+ class CryptAPICurrency(BaseModel):
26
+ """CryptAPI specific currency model."""
27
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
28
+
29
+ currency_code: str = Field(..., description="Currency symbol (e.g., BTC, ETH)")
30
+ name: str = Field(..., description="Full currency name")
31
+ minimum_transaction: Optional[Decimal] = Field(None, description="Minimum transaction amount")
32
+ maximum_transaction: Optional[Decimal] = Field(None, description="Maximum transaction amount")
33
+ fee_percent: Optional[Decimal] = Field(None, description="Fee percentage")
34
+ logo: Optional[str] = Field(None, description="Currency logo URL")
35
+
36
+
37
+ class CryptAPINetwork(BaseModel):
38
+ """CryptAPI specific network model."""
39
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
40
+
41
+ currency: str = Field(..., description="Currency code this network belongs to")
42
+ network: str = Field(..., description="Network code (e.g., mainnet, testnet)")
43
+ name: str = Field(..., description="Network display name")
44
+ confirmations: int = Field(1, description="Required confirmations")
45
+ fee: Optional[Decimal] = Field(None, description="Network fee")
46
+
47
+
48
+ class CryptAPIPaymentRequest(BaseModel):
49
+ """CryptAPI payment creation request."""
50
+ ticker: str = Field(..., description="Currency ticker")
51
+ callback: str = Field(..., description="Callback URL")
52
+ address: Optional[str] = Field(None, description="Destination address")
53
+ pending: bool = Field(False, description="Accept pending transactions")
54
+ confirmations: int = Field(1, description="Required confirmations")
55
+ email: Optional[str] = Field(None, description="Email for notifications")
56
+ post: int = Field(0, description="POST data format")
57
+ json: int = Field(1, description="JSON response format")
58
+ priority: Optional[str] = Field(None, description="Priority level")
59
+ multi_token: bool = Field(False, description="Multi-token support")
60
+ convert: int = Field(1, description="Convert amounts")
61
+
62
+
63
+ class CryptAPIPaymentResponse(BaseModel):
64
+ """CryptAPI payment creation response."""
65
+ address_in: str = Field(..., description="Payment address")
66
+ address_out: Optional[str] = Field(None, description="Destination address")
67
+ callback_url: str = Field(..., description="Callback URL")
68
+ priority: Optional[str] = Field(None, description="Priority level")
69
+ minimum: Optional[Decimal] = Field(None, description="Minimum amount")
70
+
71
+
72
+ class CryptAPICallback(BaseModel):
73
+ """CryptAPI webhook callback data according to official documentation."""
74
+ model_config = ConfigDict(validate_assignment=True, extra="allow") # Allow extra fields for custom params
75
+
76
+ # Required fields from documentation
77
+ uuid: Optional[str] = Field(None, description="Unique identifier for each payment transaction")
78
+ address_in: str = Field(..., description="CryptAPI-generated payment address")
79
+ address_out: str = Field(..., description="Your destination address(es)")
80
+ txid_in: str = Field(..., description="Transaction hash of customer's payment")
81
+ coin: str = Field(..., description="Cryptocurrency ticker")
82
+ price: Optional[Decimal] = Field(None, description="Cryptocurrency price in USD")
83
+ pending: Optional[int] = Field(0, description="1=pending webhook, 0=confirmed webhook")
84
+
85
+ # Confirmed webhook only fields
86
+ txid_out: Optional[str] = Field(None, description="CryptAPI's forwarding transaction hash")
87
+ confirmations: Optional[int] = Field(None, description="Number of blockchain confirmations")
88
+ value_coin: Optional[Decimal] = Field(None, description="Payment amount before fees")
89
+ value_forwarded_coin: Optional[Decimal] = Field(None, description="Amount forwarded after fees")
90
+ fee_coin: Optional[Decimal] = Field(None, description="CryptAPI service fee")
91
+
92
+ # Optional conversion fields (when convert=1)
93
+ value_coin_convert: Optional[str] = Field(None, description="JSON FIAT conversions of value_coin")
94
+ value_forwarded_coin_convert: Optional[str] = Field(None, description="JSON FIAT conversions of value_forwarded_coin")
95
+
96
+ @field_validator('pending')
97
+ @classmethod
98
+ def validate_pending(cls, v):
99
+ """Validate pending field is 0 or 1."""
100
+ if v not in [0, 1]:
101
+ raise ValueError("pending must be 0 (confirmed) or 1 (pending)")
102
+ return v
103
+
104
+
105
+
106
+ class CryptAPIInfoResponse(BaseModel):
107
+ """CryptAPI info endpoint response."""
108
+ ticker: str = Field(..., description="Currency ticker")
109
+ minimum_transaction: Decimal = Field(..., description="Minimum transaction amount")
110
+ maximum_transaction: Optional[Decimal] = Field(None, description="Maximum transaction amount")
111
+ fee_percent: Decimal = Field(..., description="Fee percentage")
112
+ network_fee: Decimal = Field(..., description="Network fee")
113
+ prices: Dict[str, Decimal] = Field(..., description="Price conversions")
114
+
115
+
116
+ class CryptAPIEstimateFeeResponse(BaseModel):
117
+ """CryptAPI fee estimation response."""
118
+ estimated_cost: Decimal = Field(..., description="Estimated cost")
119
+ estimated_cost_currency: Dict[str, Decimal] = Field(..., description="Cost in different currencies")
120
+
121
+
122
+ class CryptAPIConvertResponse(BaseModel):
123
+ """CryptAPI currency conversion response."""
124
+ value_coin: Decimal = Field(..., description="Value in cryptocurrency")
125
+ exchange_rate: Decimal = Field(..., description="Exchange rate used")
126
+
127
+
128
+ class CryptAPIQRCodeResponse(BaseModel):
129
+ """CryptAPI QR code response."""
130
+ qr_code: str = Field(..., description="Base64 encoded QR code image")
131
+ payment_uri: str = Field(..., description="Payment URI for QR code")
132
+
133
+
134
+ class CryptAPILogsResponse(BaseModel):
135
+ """CryptAPI logs response."""
136
+ callbacks: List[Dict[str, Any]] = Field(default_factory=list, description="Callback logs")
137
+ payments: List[Dict[str, Any]] = Field(default_factory=list, description="Payment logs")
138
+
139
+
140
+ class CryptAPISupportedCoinsResponse(BaseModel):
141
+ """CryptAPI supported coins response."""
142
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
143
+
144
+ currencies: List[CryptAPICurrency] = Field(..., description="List of supported currencies")
145
+
146
+
147
+ # =============================================================================
148
+ # MONITORING & HEALTH CHECK MODELS
149
+ # =============================================================================
150
+
151
+ class CryptAPIInfoResponse(BaseModel):
152
+ """CryptAPI /btc/info/ response schema for health checks."""
153
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
154
+
155
+ coin: str = Field(..., description="Cryptocurrency name")
156
+ logo: str = Field(..., description="Logo URL")
157
+ ticker: str = Field(..., description="Currency ticker")
158
+ minimum_transaction: int = Field(..., description="Minimum transaction in satoshis")
159
+ minimum_transaction_coin: str = Field(..., description="Minimum transaction in coin units")
160
+ minimum_fee: int = Field(..., description="Minimum fee in satoshis")
161
+ minimum_fee_coin: str = Field(..., description="Minimum fee in coin units")
162
+ fee_percent: str = Field(..., description="Fee percentage")
163
+ network_fee_estimation: str = Field(..., description="Network fee estimation")
164
+ status: str = Field(..., description="API status")
165
+ prices: Dict[str, str] = Field(..., description="Prices in various fiat currencies")
166
+ prices_updated: str = Field(..., description="Prices last updated timestamp")
167
+
168
+ @field_validator('status')
169
+ @classmethod
170
+ def validate_status(cls, v):
171
+ """Validate that status is success."""
172
+ if v != 'success':
173
+ raise ValueError(f"Expected status 'success', got '{v}'")
174
+ return v
175
+
176
+ @field_validator('prices')
177
+ @classmethod
178
+ def validate_prices_not_empty(cls, v):
179
+ """Validate that prices dict is not empty."""
180
+ if not v:
181
+ raise ValueError("Prices dictionary cannot be empty")
182
+ return v
183
+
184
+ def get_usd_price(self) -> Optional[Decimal]:
185
+ """Get USD price as Decimal."""
186
+ usd_price = self.prices.get('USD')
187
+ if usd_price:
188
+ try:
189
+ return Decimal(usd_price)
190
+ except:
191
+ return None
192
+ return None