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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/newsletter/signals.py +9 -8
- django_cfg/apps/payments/__init__.py +8 -0
- django_cfg/apps/payments/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/apps.py +22 -0
- django_cfg/apps/payments/config/__init__.py +87 -0
- django_cfg/apps/payments/config/module.py +162 -0
- django_cfg/apps/payments/config/providers.py +93 -0
- django_cfg/apps/payments/config/settings.py +136 -0
- django_cfg/apps/payments/config/utils.py +198 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/managers/__init__.py +22 -0
- django_cfg/apps/payments/managers/api_key_manager.py +35 -0
- django_cfg/apps/payments/managers/balance_manager.py +361 -0
- django_cfg/apps/payments/managers/currency_manager.py +32 -0
- django_cfg/apps/payments/managers/payment_manager.py +44 -0
- django_cfg/apps/payments/managers/subscription_manager.py +37 -0
- django_cfg/apps/payments/managers/tariff_manager.py +29 -0
- django_cfg/apps/payments/middleware/__init__.py +13 -0
- django_cfg/apps/payments/middleware/api_access.py +261 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +1003 -0
- django_cfg/apps/payments/migrations/__init__.py +1 -0
- django_cfg/apps/payments/models/__init__.py +67 -0
- django_cfg/apps/payments/models/api_keys.py +96 -0
- django_cfg/apps/payments/models/balance.py +209 -0
- django_cfg/apps/payments/models/base.py +30 -0
- django_cfg/apps/payments/models/currencies.py +138 -0
- django_cfg/apps/payments/models/events.py +73 -0
- django_cfg/apps/payments/models/payments.py +301 -0
- django_cfg/apps/payments/models/subscriptions.py +270 -0
- django_cfg/apps/payments/models/tariffs.py +102 -0
- django_cfg/apps/payments/serializers/__init__.py +56 -0
- django_cfg/apps/payments/serializers/api_keys.py +51 -0
- django_cfg/apps/payments/serializers/balance.py +59 -0
- django_cfg/apps/payments/serializers/currencies.py +55 -0
- django_cfg/apps/payments/serializers/payments.py +62 -0
- django_cfg/apps/payments/serializers/subscriptions.py +71 -0
- django_cfg/apps/payments/serializers/tariffs.py +56 -0
- django_cfg/apps/payments/services/__init__.py +65 -0
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +449 -0
- django_cfg/apps/payments/services/core/payment_service.py +393 -0
- django_cfg/apps/payments/services/core/subscription_service.py +616 -0
- django_cfg/apps/payments/services/internal_types.py +266 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/providers/__init__.py +19 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +99 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +150 -0
- django_cfg/apps/payments/signals/payment_signals.py +127 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/urls.py +78 -0
- django_cfg/apps/payments/utils/__init__.py +42 -0
- django_cfg/apps/payments/utils/config_utils.py +243 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/payments/views/__init__.py +62 -0
- django_cfg/apps/payments/views/api_key_views.py +164 -0
- django_cfg/apps/payments/views/balance_views.py +75 -0
- django_cfg/apps/payments/views/currency_views.py +111 -0
- django_cfg/apps/payments/views/payment_views.py +111 -0
- django_cfg/apps/payments/views/subscription_views.py +135 -0
- django_cfg/apps/payments/views/tariff_views.py +131 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/core/config.py +6 -0
- django_cfg/models/revolution.py +14 -0
- django_cfg/modules/base.py +9 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/RECORD +92 -14
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
- {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,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
|