django-cfg 1.2.21__py3-none-any.whl → 1.2.23__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 (92) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/newsletter/signals.py +9 -8
  3. django_cfg/apps/payments/__init__.py +8 -0
  4. django_cfg/apps/payments/admin/__init__.py +23 -0
  5. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  6. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  7. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  8. django_cfg/apps/payments/admin/filters.py +259 -0
  9. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  10. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  11. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  12. django_cfg/apps/payments/apps.py +22 -0
  13. django_cfg/apps/payments/config/__init__.py +87 -0
  14. django_cfg/apps/payments/config/module.py +162 -0
  15. django_cfg/apps/payments/config/providers.py +93 -0
  16. django_cfg/apps/payments/config/settings.py +136 -0
  17. django_cfg/apps/payments/config/utils.py +198 -0
  18. django_cfg/apps/payments/decorators.py +291 -0
  19. django_cfg/apps/payments/managers/__init__.py +22 -0
  20. django_cfg/apps/payments/managers/api_key_manager.py +35 -0
  21. django_cfg/apps/payments/managers/balance_manager.py +361 -0
  22. django_cfg/apps/payments/managers/currency_manager.py +32 -0
  23. django_cfg/apps/payments/managers/payment_manager.py +44 -0
  24. django_cfg/apps/payments/managers/subscription_manager.py +37 -0
  25. django_cfg/apps/payments/managers/tariff_manager.py +29 -0
  26. django_cfg/apps/payments/middleware/__init__.py +13 -0
  27. django_cfg/apps/payments/middleware/api_access.py +261 -0
  28. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  29. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  30. django_cfg/apps/payments/migrations/0001_initial.py +1003 -0
  31. django_cfg/apps/payments/migrations/__init__.py +1 -0
  32. django_cfg/apps/payments/models/__init__.py +67 -0
  33. django_cfg/apps/payments/models/api_keys.py +96 -0
  34. django_cfg/apps/payments/models/balance.py +209 -0
  35. django_cfg/apps/payments/models/base.py +30 -0
  36. django_cfg/apps/payments/models/currencies.py +138 -0
  37. django_cfg/apps/payments/models/events.py +73 -0
  38. django_cfg/apps/payments/models/payments.py +301 -0
  39. django_cfg/apps/payments/models/subscriptions.py +270 -0
  40. django_cfg/apps/payments/models/tariffs.py +102 -0
  41. django_cfg/apps/payments/serializers/__init__.py +56 -0
  42. django_cfg/apps/payments/serializers/api_keys.py +51 -0
  43. django_cfg/apps/payments/serializers/balance.py +59 -0
  44. django_cfg/apps/payments/serializers/currencies.py +55 -0
  45. django_cfg/apps/payments/serializers/payments.py +62 -0
  46. django_cfg/apps/payments/serializers/subscriptions.py +71 -0
  47. django_cfg/apps/payments/serializers/tariffs.py +56 -0
  48. django_cfg/apps/payments/services/__init__.py +65 -0
  49. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  50. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  51. django_cfg/apps/payments/services/cache/base.py +30 -0
  52. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  53. django_cfg/apps/payments/services/core/__init__.py +17 -0
  54. django_cfg/apps/payments/services/core/balance_service.py +449 -0
  55. django_cfg/apps/payments/services/core/payment_service.py +393 -0
  56. django_cfg/apps/payments/services/core/subscription_service.py +616 -0
  57. django_cfg/apps/payments/services/internal_types.py +266 -0
  58. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  59. django_cfg/apps/payments/services/providers/__init__.py +19 -0
  60. django_cfg/apps/payments/services/providers/base.py +137 -0
  61. django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
  62. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  63. django_cfg/apps/payments/services/providers/registry.py +99 -0
  64. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  65. django_cfg/apps/payments/signals/__init__.py +13 -0
  66. django_cfg/apps/payments/signals/api_key_signals.py +150 -0
  67. django_cfg/apps/payments/signals/payment_signals.py +127 -0
  68. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  69. django_cfg/apps/payments/urls.py +78 -0
  70. django_cfg/apps/payments/utils/__init__.py +42 -0
  71. django_cfg/apps/payments/utils/config_utils.py +243 -0
  72. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  73. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  74. django_cfg/apps/payments/views/__init__.py +62 -0
  75. django_cfg/apps/payments/views/api_key_views.py +164 -0
  76. django_cfg/apps/payments/views/balance_views.py +75 -0
  77. django_cfg/apps/payments/views/currency_views.py +111 -0
  78. django_cfg/apps/payments/views/payment_views.py +111 -0
  79. django_cfg/apps/payments/views/subscription_views.py +135 -0
  80. django_cfg/apps/payments/views/tariff_views.py +131 -0
  81. django_cfg/apps/support/signals.py +16 -4
  82. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  83. django_cfg/core/config.py +6 -0
  84. django_cfg/models/revolution.py +14 -0
  85. django_cfg/modules/base.py +9 -0
  86. django_cfg/modules/django_email.py +42 -4
  87. django_cfg/modules/django_unfold/dashboard.py +20 -0
  88. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
  89. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/RECORD +92 -14
  90. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
  91. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
  92. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,266 @@
1
+ """
2
+ Internal Service Types - ONLY for inter-service communication.
3
+
4
+ DO NOT duplicate Django ORM or DRF! Only for:
5
+ 1. Providers -> Services (external API response validation)
6
+ 2. Service -> Service (internal operations)
7
+ 3. Configuration (settings and parameters)
8
+ """
9
+
10
+ from pydantic import BaseModel, Field, ConfigDict
11
+ from decimal import Decimal
12
+ from datetime import datetime
13
+ from typing import Optional, Dict, Any
14
+ from enum import Enum
15
+
16
+
17
+ # =============================================================================
18
+ # PROVIDERS - External API response validation
19
+ # =============================================================================
20
+
21
+ class ProviderResponse(BaseModel):
22
+ """Validation for any provider response"""
23
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
24
+
25
+ success: bool
26
+ provider_payment_id: Optional[str] = None
27
+ payment_url: Optional[str] = None
28
+ pay_amount: Optional[Decimal] = None
29
+ pay_currency: Optional[str] = None
30
+ pay_address: Optional[str] = None
31
+ status: Optional[str] = None
32
+ error_message: Optional[str] = None
33
+ # Legacy fields for backward compatibility with tests
34
+ amount: Optional[Decimal] = None
35
+ currency: Optional[str] = None
36
+ payment_id: Optional[str] = None
37
+ payment_status: Optional[str] = None
38
+ currency_code: Optional[str] = None
39
+
40
+
41
+ class WebhookData(BaseModel):
42
+ """Provider webhook validation"""
43
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
44
+
45
+ provider_payment_id: str
46
+ status: str
47
+ pay_amount: Optional[Decimal] = None
48
+ pay_currency: Optional[str] = None
49
+ actually_paid: Optional[Decimal] = None
50
+ order_id: Optional[str] = None
51
+ signature: Optional[str] = None
52
+ error_message: Optional[str] = None
53
+
54
+
55
+ # =============================================================================
56
+ # INTER-SERVICE OPERATIONS - Service-to-service typing
57
+ # =============================================================================
58
+
59
+ class ServiceOperationResult(BaseModel):
60
+ """Result of inter-service operation"""
61
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
62
+
63
+ success: bool
64
+ error_code: Optional[str] = None
65
+ error_message: Optional[str] = None
66
+ data: Dict[str, Any] = Field(default_factory=dict)
67
+
68
+
69
+ class BalanceUpdateRequest(BaseModel):
70
+ """Balance update request between services"""
71
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
72
+
73
+ user_id: int = Field(gt=0)
74
+ amount: Decimal
75
+ source: str
76
+ reference_id: Optional[str] = None
77
+ metadata: Dict[str, Any] = Field(default_factory=dict)
78
+
79
+
80
+ class AccessCheckRequest(BaseModel):
81
+ """Access check request between services"""
82
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
83
+
84
+ user_id: int = Field(gt=0)
85
+ endpoint_group: str
86
+ use_cache: bool = True
87
+
88
+
89
+ class AccessCheckResult(BaseModel):
90
+ """Access check result"""
91
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
92
+
93
+ allowed: bool
94
+ subscription_id: Optional[str] = None
95
+ reason: Optional[str] = None
96
+ remaining_requests: Optional[int] = None
97
+ usage_percentage: Optional[float] = None
98
+
99
+
100
+ # =============================================================================
101
+ # CONFIGURATION - Service settings
102
+ # =============================================================================
103
+
104
+ class RedisConfig(BaseModel):
105
+ """Redis configuration"""
106
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
107
+
108
+ host: str = "localhost"
109
+ port: int = 6379
110
+ db: int = 0
111
+ password: Optional[str] = None
112
+ max_connections: int = 10
113
+ timeout_seconds: int = 5
114
+
115
+
116
+ class ProviderConfig(BaseModel):
117
+ """Base provider configuration"""
118
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
119
+
120
+ enabled: bool = True
121
+ api_key: str
122
+ sandbox: bool = False
123
+ timeout_seconds: int = 30
124
+ max_retries: int = 3
125
+
126
+
127
+ # =============================================================================
128
+ # CACHE OPERATIONS - Minimal cache typing
129
+ # =============================================================================
130
+
131
+ class CacheKey(BaseModel):
132
+ """Cache key typing"""
133
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
134
+
135
+ key: str
136
+ ttl_seconds: Optional[int] = None
137
+
138
+
139
+ class RateLimitResult(BaseModel):
140
+ """Rate limit check result"""
141
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
142
+
143
+ allowed: bool
144
+ remaining: int
145
+ reset_at: datetime
146
+ retry_after_seconds: Optional[int] = None
147
+
148
+
149
+ # =============================================================================
150
+ # SERVICE RESPONSE MODELS - Typed responses for service methods
151
+ # =============================================================================
152
+
153
+ class PaymentCreationResult(BaseModel):
154
+ """Payment creation response"""
155
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
156
+
157
+ success: bool
158
+ payment_id: Optional[str] = None
159
+ provider_payment_id: Optional[str] = None
160
+ payment_url: Optional[str] = None
161
+ error: Optional[str] = None
162
+
163
+
164
+ class WebhookProcessingResult(BaseModel):
165
+ """Webhook processing response"""
166
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
167
+
168
+ success: bool
169
+ payment_id: Optional[str] = None
170
+ status_updated: bool = False
171
+ balance_updated: bool = False
172
+ error: Optional[str] = None
173
+
174
+
175
+ class PaymentStatusResult(BaseModel):
176
+ """Payment status response"""
177
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
178
+
179
+ payment_id: str
180
+ status: str
181
+ amount_usd: Decimal
182
+ currency_code: str
183
+ provider: str
184
+ provider_payment_id: Optional[str] = None
185
+ created_at: datetime
186
+ updated_at: datetime
187
+
188
+
189
+ class UserBalanceResult(BaseModel):
190
+ """User balance response"""
191
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
192
+
193
+ id: str
194
+ user_id: int
195
+ available_balance: Decimal
196
+ total_balance: Decimal
197
+ reserved_balance: Decimal
198
+ last_updated: datetime
199
+ created_at: datetime
200
+
201
+
202
+ class TransferResult(BaseModel):
203
+ """Funds transfer response"""
204
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
205
+
206
+ success: bool
207
+ transaction_id: Optional[str] = None
208
+ from_user_id: int
209
+ to_user_id: int
210
+ amount: Decimal
211
+ error: Optional[str] = None
212
+ error_code: Optional[str] = None
213
+
214
+
215
+ class TransactionInfo(BaseModel):
216
+ """Transaction information"""
217
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
218
+
219
+ id: str
220
+ user_id: int
221
+ transaction_type: str
222
+ amount: Decimal
223
+ balance_after: Decimal
224
+ source: str
225
+ reference_id: Optional[str] = None
226
+ description: Optional[str] = None
227
+ created_at: datetime
228
+
229
+
230
+ class EndpointGroupInfo(BaseModel):
231
+ """Endpoint group information"""
232
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
233
+
234
+ id: str
235
+ name: str
236
+ display_name: str
237
+
238
+
239
+ class SubscriptionInfo(BaseModel):
240
+ """Subscription information"""
241
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
242
+
243
+ id: str
244
+ endpoint_group: EndpointGroupInfo
245
+ status: str
246
+ tier: str
247
+ monthly_price: Decimal
248
+ usage_current: int
249
+ usage_limit: int
250
+ usage_percentage: float
251
+ remaining_requests: int
252
+ expires_at: datetime
253
+ next_billing: Optional[datetime] = None
254
+ created_at: datetime
255
+
256
+
257
+ class SubscriptionAnalytics(BaseModel):
258
+ """Subscription analytics response"""
259
+ model_config = ConfigDict(validate_assignment=True, extra="forbid")
260
+
261
+ period: Dict[str, Any] = Field(default_factory=dict)
262
+ total_revenue: Decimal
263
+ active_subscriptions: int
264
+ new_subscriptions: int
265
+ churned_subscriptions: int
266
+ error: Optional[str] = None
@@ -0,0 +1,8 @@
1
+ """
2
+ Service layer middleware for payments.
3
+
4
+ TODO: Implement service middleware when needed.
5
+ """
6
+
7
+ # Placeholder for future service middleware
8
+ __all__ = []
@@ -0,0 +1,19 @@
1
+ """
2
+ Payment provider services.
3
+
4
+ All payment provider implementations and abstractions.
5
+ """
6
+
7
+ from .base import PaymentProvider
8
+ from .registry import ProviderRegistry
9
+ from .nowpayments import NowPaymentsProvider, NowPaymentsConfig
10
+ from .cryptapi import CryptAPIProvider, CryptAPIConfig
11
+
12
+ __all__ = [
13
+ 'PaymentProvider',
14
+ 'ProviderRegistry',
15
+ 'NowPaymentsProvider',
16
+ 'NowPaymentsConfig',
17
+ 'CryptAPIProvider',
18
+ 'CryptAPIConfig',
19
+ ]
@@ -0,0 +1,137 @@
1
+ """
2
+ Base payment provider interface.
3
+
4
+ Abstract base class for all payment providers.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from typing import Optional, List
9
+ from decimal import Decimal
10
+
11
+ from ..internal_types import ProviderResponse, WebhookData
12
+
13
+
14
+ class PaymentProvider(ABC):
15
+ """Abstract base class for payment providers."""
16
+
17
+ def __init__(self, config: dict):
18
+ """Initialize provider with config."""
19
+ self.config = config
20
+ self.name = self.__class__.__name__.lower().replace('provider', '')
21
+ self.enabled = config.get('enabled', True)
22
+
23
+ @abstractmethod
24
+ def create_payment(self, payment_data: dict) -> ProviderResponse:
25
+ """
26
+ Create a payment request.
27
+
28
+ Args:
29
+ amount: Payment amount
30
+ currency: Payment currency
31
+ **kwargs: Additional parameters (order_id, description, etc.)
32
+
33
+ Returns:
34
+ Dict with payment creation result
35
+ """
36
+ pass
37
+
38
+ @abstractmethod
39
+ def check_payment_status(self, payment_id: str) -> ProviderResponse:
40
+ """
41
+ Check payment status.
42
+
43
+ Args:
44
+ payment_id: Payment ID from provider
45
+
46
+ Returns:
47
+ Dict with payment status
48
+ """
49
+ pass
50
+
51
+ @abstractmethod
52
+ def process_webhook(self, payload: dict) -> WebhookData:
53
+ """
54
+ Process webhook payload.
55
+
56
+ Args:
57
+ payload: Webhook data from provider
58
+
59
+ Returns:
60
+ Dict with processed webhook data
61
+ """
62
+ pass
63
+
64
+ @abstractmethod
65
+ def get_supported_currencies(self) -> List[str]:
66
+ """
67
+ Get list of supported currencies.
68
+
69
+ Returns:
70
+ List of supported currency codes
71
+ """
72
+ pass
73
+
74
+ def validate_webhook(self, payload: dict, headers: Optional[dict] = None) -> bool:
75
+ """
76
+ Validate webhook signature and data.
77
+
78
+ Args:
79
+ payload: Webhook data
80
+ signature: Webhook signature (if applicable)
81
+
82
+ Returns:
83
+ True if webhook is valid
84
+ """
85
+ # Default implementation - providers can override
86
+ return True
87
+
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
+
116
+ def check_api_status(self) -> bool:
117
+ """
118
+ Check if provider API is available.
119
+
120
+ Returns:
121
+ True if API is available
122
+ """
123
+ # Optional method - providers can override
124
+ return True
125
+
126
+ def is_enabled(self) -> bool:
127
+ """Check if provider is enabled."""
128
+ return self.enabled
129
+
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
+ }
@@ -0,0 +1,262 @@
1
+ """
2
+ CryptAPI provider implementation.
3
+
4
+ Crypto payment provider using CryptAPI service.
5
+ """
6
+
7
+ import logging
8
+ import requests
9
+ from typing import Optional, List
10
+ from decimal import Decimal
11
+ from pydantic import BaseModel, Field
12
+
13
+ from .base import PaymentProvider
14
+ from ..internal_types import ProviderResponse, WebhookData
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class CryptAPIConfig(BaseModel):
20
+ """CryptAPI provider configuration."""
21
+ own_address: str = Field(..., description="Your cryptocurrency address")
22
+ callback_url: str = Field(..., description="Webhook callback URL")
23
+ convert_payments: bool = Field(default=True, description="Auto-convert payments")
24
+ multi_token: bool = Field(default=True, description="Support multi-token payments")
25
+ priority: str = Field(default='default', description="Transaction priority")
26
+ enabled: bool = Field(default=True, description="Provider enabled")
27
+
28
+
29
+ class CryptAPIException(Exception):
30
+ """CryptAPI specific exception."""
31
+ pass
32
+
33
+
34
+ class CryptAPIProvider(PaymentProvider):
35
+ """CryptAPI cryptocurrency payment provider."""
36
+
37
+ CRYPTAPI_URL = 'https://api.cryptapi.io/'
38
+
39
+ def __init__(self, config: CryptAPIConfig):
40
+ """Initialize CryptAPI provider."""
41
+ super().__init__(config.dict())
42
+ self.config = config
43
+ self.own_address = config.own_address
44
+ self.callback_url = config.callback_url
45
+ self.convert_payments = config.convert_payments
46
+ self.multi_token = config.multi_token
47
+ self.priority = config.priority
48
+
49
+ def _make_request(self, coin: str, endpoint: str, params: Optional[dict] = None) -> Optional[dict]:
50
+ """Make HTTP request to CryptAPI."""
51
+ try:
52
+ if coin:
53
+ coin = coin.replace('/', '_')
54
+ url = f"{self.CRYPTAPI_URL}{coin}/{endpoint}/"
55
+ else:
56
+ url = f"{self.CRYPTAPI_URL}{endpoint}/"
57
+
58
+ response = requests.get(url, params=params or {}, timeout=30)
59
+ response.raise_for_status()
60
+
61
+ result = response.json()
62
+
63
+ # Check for API errors
64
+ if 'error' in result:
65
+ logger.error(f"CryptAPI error: {result['error']}")
66
+ return None
67
+
68
+ return result
69
+
70
+ except requests.exceptions.RequestException as e:
71
+ logger.error(f"CryptAPI request failed: {e}")
72
+ return None
73
+ except Exception as e:
74
+ logger.error(f"Unexpected CryptAPI error: {e}")
75
+ return None
76
+
77
+ def create_payment(self, payment_data: dict) -> ProviderResponse:
78
+ """Create payment address via CryptAPI."""
79
+ try:
80
+ amount = Decimal(str(payment_data['amount']))
81
+ currency = payment_data['currency'].lower()
82
+ order_id = payment_data.get('order_id', f'payment_{int(amount * 100)}')
83
+
84
+ # Build callback URL with parameters
85
+ callback_params = {
86
+ 'order_id': order_id,
87
+ 'amount': str(amount)
88
+ }
89
+
90
+ # Create payment address
91
+ params = {
92
+ 'address': self.own_address,
93
+ 'callback': self.callback_url,
94
+ 'convert': 1 if self.convert_payments else 0,
95
+ 'multi_token': 1 if self.multi_token else 0,
96
+ 'priority': self.priority,
97
+ **callback_params
98
+ }
99
+
100
+ response = self._make_request(currency, 'create', params)
101
+
102
+ if response and 'address_in' in response:
103
+ return ProviderResponse(
104
+ success=True,
105
+ provider_payment_id=response['address_in'], # Use address as payment ID
106
+ payment_url=None, # CryptAPI doesn't provide payment URLs
107
+ pay_address=response['address_in'],
108
+ amount=amount,
109
+ currency=currency.upper(),
110
+ status='pending'
111
+ )
112
+ else:
113
+ return ProviderResponse(
114
+ success=False,
115
+ error_message='Failed to create payment address'
116
+ )
117
+
118
+ except Exception as e:
119
+ logger.error(f"CryptAPI create_payment error: {e}")
120
+ return ProviderResponse(
121
+ success=False,
122
+ error_message=str(e)
123
+ )
124
+
125
+ def check_payment_status(self, payment_id: str) -> ProviderResponse:
126
+ """Check payment status via CryptAPI."""
127
+ try:
128
+ # For CryptAPI, payment_id is the address
129
+ # We need to check logs to see if payment was received
130
+ # This is a limitation of CryptAPI - no direct status check by address
131
+
132
+ # Return pending status as CryptAPI uses callbacks for status updates
133
+ return ProviderResponse(
134
+ success=True,
135
+ provider_payment_id=payment_id,
136
+ status='pending',
137
+ pay_address=payment_id,
138
+ amount=Decimal('0'), # Unknown without logs
139
+ currency='unknown'
140
+ )
141
+
142
+ except Exception as e:
143
+ logger.error(f"CryptAPI check_payment_status error: {e}")
144
+ return ProviderResponse(
145
+ success=False,
146
+ error_message=str(e)
147
+ )
148
+
149
+ def process_webhook(self, payload: dict) -> WebhookData:
150
+ """Process CryptAPI webhook/callback."""
151
+ try:
152
+ # CryptAPI sends callbacks with these parameters:
153
+ # - address_in: payment address
154
+ # - address_out: your address
155
+ # - txid_in: transaction ID
156
+ # - txid_out: forwarding transaction ID (if applicable)
157
+ # - confirmations: number of confirmations
158
+ # - value: amount received
159
+ # - value_coin: amount in coin
160
+ # - value_forwarded: amount forwarded
161
+ # - coin: cryptocurrency
162
+ # - pending: 0 or 1
163
+
164
+ confirmations = int(payload.get('confirmations', 0))
165
+ pending = int(payload.get('pending', 1))
166
+
167
+ # Determine status based on confirmations and pending flag
168
+ if pending == 1:
169
+ status = 'pending'
170
+ elif confirmations >= 1:
171
+ status = 'completed'
172
+ else:
173
+ status = 'processing'
174
+
175
+ return WebhookData(
176
+ provider_payment_id=payload.get('address_in', ''),
177
+ status=status,
178
+ pay_amount=Decimal(str(payload.get('value_coin', 0))),
179
+ actually_paid=Decimal(str(payload.get('value_coin', 0))),
180
+ order_id=payload.get('order_id'), # Custom parameter we sent
181
+ signature=payload.get('txid_in') # Use transaction ID as signature
182
+ )
183
+
184
+ except Exception as e:
185
+ logger.error(f"CryptAPI webhook processing error: {e}")
186
+ raise
187
+
188
+ def get_supported_currencies(self) -> List[str]:
189
+ """Get list of supported currencies."""
190
+ try:
191
+ response = self._make_request('', 'info')
192
+
193
+ if response and isinstance(response, dict):
194
+ # CryptAPI returns a dict with coin info
195
+ return list(response.keys())
196
+ else:
197
+ # Fallback currencies
198
+ return ['BTC', 'ETH', 'LTC', 'BCH', 'XMR', 'TRX']
199
+
200
+ except Exception as e:
201
+ logger.error(f"Error getting supported currencies: {e}")
202
+ return ['BTC', 'ETH', 'LTC'] # Minimal fallback
203
+
204
+ def get_minimum_payment_amount(self, currency_from: str, currency_to: str = 'usd') -> Optional[Decimal]:
205
+ """Get minimum payment amount for currency."""
206
+ try:
207
+ response = self._make_request(currency_from.lower(), 'info')
208
+
209
+ if response and 'minimum_transaction' in response:
210
+ return Decimal(str(response['minimum_transaction']))
211
+
212
+ return None
213
+
214
+ except Exception as e:
215
+ logger.error(f"Error getting minimum amount: {e}")
216
+ return None
217
+
218
+ def estimate_payment_amount(self, amount: Decimal, currency_code: str) -> Optional[dict]:
219
+ """Estimate payment amount - CryptAPI doesn't provide this."""
220
+ # CryptAPI doesn't have a direct estimation API
221
+ # Would need to use external price APIs
222
+ return None
223
+
224
+ def validate_webhook(self, payload: dict, headers: Optional[dict] = None) -> bool:
225
+ """Validate CryptAPI webhook."""
226
+ try:
227
+ # CryptAPI doesn't use HMAC signatures
228
+ # Validation is done by checking if the callback came from their servers
229
+ # and contains expected parameters
230
+
231
+ required_fields = ['address_in', 'value', 'txid_in', 'confirmations']
232
+
233
+ for field in required_fields:
234
+ if field not in payload:
235
+ logger.warning(f"Missing required field in CryptAPI webhook: {field}")
236
+ return False
237
+
238
+ # Basic validation passed
239
+ return True
240
+
241
+ except Exception as e:
242
+ logger.error(f"CryptAPI webhook validation error: {e}")
243
+ return False
244
+
245
+ def check_api_status(self) -> bool:
246
+ """Check if CryptAPI is available."""
247
+ try:
248
+ response = self._make_request('', 'info')
249
+ return response is not None
250
+ except:
251
+ return False
252
+
253
+ def get_logs(self, callback_url: str) -> Optional[dict]:
254
+ """Get payment logs for a callback URL."""
255
+ try:
256
+ params = {'callback': callback_url}
257
+ # Note: This would need a specific coin, but we don't know which one
258
+ # This is a limitation of the current implementation
259
+ return None
260
+ except Exception as e:
261
+ logger.error(f"Error getting logs: {e}")
262
+ return None