django-cfg 1.2.29__py3-none-any.whl → 1.2.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/payments/admin/__init__.py +3 -2
- django_cfg/apps/payments/admin/balance_admin.py +18 -18
- django_cfg/apps/payments/admin/currencies_admin.py +319 -131
- django_cfg/apps/payments/admin/payments_admin.py +15 -4
- django_cfg/apps/payments/config/module.py +2 -2
- django_cfg/apps/payments/config/utils.py +2 -2
- django_cfg/apps/payments/decorators.py +2 -2
- django_cfg/apps/payments/management/commands/README.md +95 -127
- django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
- django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
- django_cfg/apps/payments/managers/__init__.py +3 -2
- django_cfg/apps/payments/managers/balance_manager.py +2 -2
- django_cfg/apps/payments/managers/currency_manager.py +272 -49
- django_cfg/apps/payments/managers/payment_manager.py +161 -13
- django_cfg/apps/payments/middleware/api_access.py +2 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
- django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
- django_cfg/apps/payments/models/__init__.py +3 -2
- django_cfg/apps/payments/models/currencies.py +187 -71
- django_cfg/apps/payments/models/payments.py +3 -2
- django_cfg/apps/payments/serializers/__init__.py +3 -2
- django_cfg/apps/payments/serializers/currencies.py +20 -12
- django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
- django_cfg/apps/payments/services/core/balance_service.py +2 -2
- django_cfg/apps/payments/services/core/fallback_service.py +2 -2
- django_cfg/apps/payments/services/core/payment_service.py +3 -6
- django_cfg/apps/payments/services/core/subscription_service.py +4 -7
- django_cfg/apps/payments/services/internal_types.py +171 -7
- django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
- django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
- django_cfg/apps/payments/services/providers/base.py +144 -43
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
- django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
- django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
- django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
- django_cfg/apps/payments/services/providers/registry.py +294 -11
- django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
- django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
- django_cfg/apps/payments/services/security/error_handler.py +6 -8
- django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
- django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/payment_signals.py +11 -5
- django_cfg/apps/payments/signals/subscription_signals.py +2 -2
- django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
- django_cfg/apps/payments/templates/payments/base.html +4 -4
- django_cfg/apps/payments/templates/payments/components/payment_card.html +6 -6
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +4 -4
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +14 -7
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +2 -2
- django_cfg/apps/payments/templates/payments/components/status_badge.html +8 -1
- django_cfg/apps/payments/templates/payments/components/status_overview.html +34 -30
- django_cfg/apps/payments/templates/payments/dashboard.html +202 -290
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
- django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
- django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
- django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
- django_cfg/apps/payments/templates/payments/stats.html +261 -0
- django_cfg/apps/payments/templates/payments/test.html +213 -0
- django_cfg/apps/payments/urls.py +3 -1
- django_cfg/apps/payments/{urls_templates.py → urls_admin.py} +6 -0
- django_cfg/apps/payments/utils/__init__.py +1 -3
- django_cfg/apps/payments/utils/billing_utils.py +2 -2
- django_cfg/apps/payments/utils/config_utils.py +2 -8
- django_cfg/apps/payments/utils/validation_utils.py +2 -2
- django_cfg/apps/payments/views/__init__.py +3 -2
- django_cfg/apps/payments/views/currency_views.py +31 -20
- django_cfg/apps/payments/views/payment_views.py +2 -2
- django_cfg/apps/payments/views/templates/ajax.py +141 -2
- django_cfg/apps/payments/views/templates/base.py +21 -13
- django_cfg/apps/payments/views/templates/payment_detail.py +1 -1
- django_cfg/apps/payments/views/templates/payment_management.py +34 -40
- django_cfg/apps/payments/views/templates/stats.py +8 -4
- django_cfg/apps/payments/views/webhook_views.py +2 -2
- django_cfg/apps/payments/viewsets.py +3 -2
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/core/config.py +35 -0
- django_cfg/models/payments.py +2 -8
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_unfold/dashboard.py +7 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/METADATA +2 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/RECORD +118 -96
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -5,15 +5,20 @@ Manager for Currency model.
|
|
5
5
|
from django.db import models
|
6
6
|
from django.utils import timezone
|
7
7
|
from datetime import timedelta
|
8
|
-
from typing import List, Optional
|
8
|
+
from typing import List, Optional, TYPE_CHECKING
|
9
|
+
from decimal import Decimal
|
10
|
+
|
11
|
+
from django_cfg.modules.django_logger import get_logger
|
12
|
+
from django_cfg.modules.django_currency import convert_currency, get_exchange_rate, CurrencyError
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from ..services.internal_types import CurrencyOptionModel
|
16
|
+
|
17
|
+
logger = get_logger("currency_manager")
|
9
18
|
|
10
19
|
|
11
20
|
class CurrencyManager(models.Manager):
|
12
|
-
"""Manager for Currency model
|
13
|
-
|
14
|
-
def active(self):
|
15
|
-
"""Get only active currencies."""
|
16
|
-
return self.filter(is_active=True)
|
21
|
+
"""Manager for clean Currency model."""
|
17
22
|
|
18
23
|
def fiat(self):
|
19
24
|
"""Get only fiat currencies."""
|
@@ -23,61 +28,279 @@ class CurrencyManager(models.Manager):
|
|
23
28
|
"""Get only cryptocurrencies."""
|
24
29
|
return self.filter(currency_type='crypto')
|
25
30
|
|
26
|
-
def active_fiat(self):
|
27
|
-
"""Get active fiat currencies."""
|
28
|
-
return self.filter(currency_type='fiat', is_active=True)
|
29
|
-
|
30
|
-
def active_crypto(self):
|
31
|
-
"""Get active cryptocurrencies."""
|
32
|
-
return self.filter(currency_type='crypto', is_active=True)
|
33
|
-
|
34
31
|
def by_code(self, code: str):
|
35
32
|
"""Get currency by code (case insensitive)."""
|
36
33
|
return self.filter(code__iexact=code).first()
|
37
34
|
|
38
|
-
def supported_for_payments(self, min_amount: float = None):
|
39
|
-
"""Get currencies supported for payments."""
|
40
|
-
queryset = self.active()
|
41
|
-
if min_amount:
|
42
|
-
queryset = queryset.filter(min_payment_amount__lte=min_amount)
|
43
|
-
return queryset
|
44
|
-
|
45
|
-
def recently_updated(self, hours: int = 24):
|
46
|
-
"""Get currencies updated within the last N hours."""
|
47
|
-
threshold = timezone.now() - timedelta(hours=hours)
|
48
|
-
return self.filter(rate_updated_at__gte=threshold)
|
49
|
-
|
50
|
-
def outdated(self, days: int = 7):
|
51
|
-
"""Get currencies with outdated rates."""
|
52
|
-
threshold = timezone.now() - timedelta(days=days)
|
53
|
-
return self.filter(
|
54
|
-
models.Q(rate_updated_at__lt=threshold) |
|
55
|
-
models.Q(rate_updated_at__isnull=True)
|
56
|
-
)
|
57
|
-
|
58
|
-
def top_crypto_by_value(self, limit: int = 10):
|
59
|
-
"""Get top cryptocurrencies by USD value."""
|
60
|
-
return self.active_crypto().order_by('-usd_rate')[:limit]
|
61
|
-
|
62
35
|
def search(self, query: str):
|
63
36
|
"""Search currencies by code or name."""
|
64
37
|
return self.filter(
|
65
38
|
models.Q(code__icontains=query) |
|
66
39
|
models.Q(name__icontains=query)
|
67
40
|
)
|
41
|
+
|
42
|
+
def get_usd_rate(self, currency_code_or_instance, force_refresh: bool = False) -> float:
|
43
|
+
"""
|
44
|
+
Get USD exchange rate for currency (with 24h cache).
|
45
|
+
|
46
|
+
Args:
|
47
|
+
currency_code_or_instance: Currency code (e.g., 'BTC') or Currency instance
|
48
|
+
force_refresh: If True, skip cache and fetch fresh rate
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
float: 1 CURRENCY = X USD
|
52
|
+
"""
|
53
|
+
try:
|
54
|
+
# Handle both Currency instance and string code
|
55
|
+
if hasattr(currency_code_or_instance, 'code'):
|
56
|
+
# Currency instance passed
|
57
|
+
currency = currency_code_or_instance
|
58
|
+
currency_code = currency.code
|
59
|
+
else:
|
60
|
+
# String code passed
|
61
|
+
currency_code = str(currency_code_or_instance).upper()
|
62
|
+
currency = self.filter(code=currency_code).first()
|
63
|
+
|
64
|
+
# Return cached rate if fresh and not forcing refresh
|
65
|
+
if not force_refresh and currency and currency.usd_rate is not None and currency.rate_updated_at:
|
66
|
+
# Check if cache is still fresh (24 hours)
|
67
|
+
if timezone.now() - currency.rate_updated_at < timedelta(hours=24):
|
68
|
+
logger.debug(f"Using cached USD rate for {currency_code}: ${float(currency.usd_rate):.8f}")
|
69
|
+
return float(currency.usd_rate)
|
70
|
+
|
71
|
+
# Cache miss, expired, or forced refresh - fetch fresh rate
|
72
|
+
logger.info(f"Fetching fresh USD rate for {currency_code} (force_refresh={force_refresh})")
|
73
|
+
rate = get_exchange_rate(currency_code, 'USD')
|
74
|
+
rate_decimal = Decimal(str(rate)).quantize(Decimal('0.00000001'))
|
75
|
+
|
76
|
+
# Update cache
|
77
|
+
if currency:
|
78
|
+
currency.usd_rate = rate_decimal
|
79
|
+
currency.rate_updated_at = timezone.now()
|
80
|
+
currency.save(update_fields=['usd_rate', 'rate_updated_at'])
|
81
|
+
logger.info(f"Updated USD rate for {currency_code}: ${rate:.8f}")
|
82
|
+
else:
|
83
|
+
logger.warning(f"Currency {currency_code} not found in database for rate caching")
|
84
|
+
|
85
|
+
return round(rate, 8)
|
86
|
+
|
87
|
+
except CurrencyError as e:
|
88
|
+
logger.warning(f"Failed to get USD rate for {currency_code}: {e}")
|
89
|
+
# Return cached rate if available, even if stale
|
90
|
+
if currency and currency.usd_rate is not None:
|
91
|
+
logger.info(f"Using stale cached rate for {currency_code} due to API error")
|
92
|
+
return float(currency.usd_rate)
|
93
|
+
return 0.0
|
94
|
+
|
95
|
+
def get_tokens_per_usd(self, currency_code: str) -> float:
|
96
|
+
"""Get how many tokens you can buy for 1 USD."""
|
97
|
+
usd_rate = self.get_usd_rate(currency_code)
|
98
|
+
if usd_rate > 0:
|
99
|
+
return round(1.0 / usd_rate, 8)
|
100
|
+
return 0.0
|
101
|
+
|
102
|
+
def convert_to_usd(self, amount: float, currency_code: str) -> float:
|
103
|
+
"""Convert currency amount to USD."""
|
104
|
+
usd_rate = self.get_usd_rate(currency_code)
|
105
|
+
return round(amount * usd_rate, 2)
|
106
|
+
|
107
|
+
def convert_from_usd(self, usd_amount: float, currency_code: str) -> float:
|
108
|
+
"""Convert USD amount to target currency."""
|
109
|
+
tokens_per_usd = self.get_tokens_per_usd(currency_code)
|
110
|
+
return round(usd_amount * tokens_per_usd, 8)
|
111
|
+
|
112
|
+
def get_or_create_normalized(self, code: str, defaults: dict = None):
|
113
|
+
"""Simple get_or_create with uppercase code normalization."""
|
114
|
+
normalized_code = code.upper().strip() if code else ''
|
115
|
+
if not normalized_code:
|
116
|
+
raise ValueError(f"Empty currency code: '{code}'")
|
117
|
+
|
118
|
+
creation_defaults = defaults or {}
|
119
|
+
creation_defaults['code'] = normalized_code
|
120
|
+
|
121
|
+
return self.get_or_create(code__iexact=normalized_code, defaults=creation_defaults)
|
122
|
+
|
123
|
+
|
124
|
+
class NetworkManager(models.Manager):
|
125
|
+
"""Manager for Network model."""
|
126
|
+
|
127
|
+
def by_code(self, code: str):
|
128
|
+
"""Get network by code (case insensitive)."""
|
129
|
+
return self.filter(code__iexact=code).first()
|
130
|
+
|
131
|
+
def get_or_create_normalized(self, code: str, defaults: dict = None):
|
132
|
+
"""Get or create network with normalized code."""
|
133
|
+
normalized_code = code.lower().strip() if code else ''
|
134
|
+
if not normalized_code:
|
135
|
+
raise ValueError(f"Empty network code: '{code}'")
|
136
|
+
|
137
|
+
creation_defaults = defaults or {}
|
138
|
+
creation_defaults['code'] = normalized_code
|
139
|
+
|
140
|
+
return self.get_or_create(code__iexact=normalized_code, defaults=creation_defaults)
|
68
141
|
|
69
142
|
|
70
|
-
class
|
71
|
-
"""Manager for
|
143
|
+
class ProviderCurrencyManager(models.Manager):
|
144
|
+
"""Manager for ProviderCurrency model."""
|
145
|
+
|
146
|
+
def enabled(self):
|
147
|
+
"""Get only enabled provider currencies."""
|
148
|
+
return self.filter(is_enabled=True)
|
149
|
+
|
150
|
+
def for_provider(self, provider_name: str):
|
151
|
+
"""Get currencies for specific provider."""
|
152
|
+
return self.filter(provider_name__iexact=provider_name)
|
153
|
+
|
154
|
+
def for_base_currency(self, currency_code: str):
|
155
|
+
"""Get provider currencies for base currency."""
|
156
|
+
return self.filter(base_currency__code__iexact=currency_code)
|
157
|
+
|
158
|
+
def for_network(self, network_code: str):
|
159
|
+
"""Get provider currencies for network."""
|
160
|
+
return self.filter(network__code__iexact=network_code)
|
161
|
+
|
162
|
+
def enabled_for_provider(self, provider_name: str):
|
163
|
+
"""Get enabled currencies for provider."""
|
164
|
+
return self.enabled().filter(provider_name__iexact=provider_name)
|
165
|
+
|
166
|
+
def popular(self):
|
167
|
+
"""Get popular currencies."""
|
168
|
+
return self.filter(is_popular=True)
|
169
|
+
|
170
|
+
def stable(self):
|
171
|
+
"""Get stable currencies."""
|
172
|
+
return self.filter(is_stable=True)
|
72
173
|
|
73
|
-
def
|
74
|
-
"""
|
75
|
-
|
174
|
+
def get_currency_options_for_provider(self, provider_name: str):
|
175
|
+
"""
|
176
|
+
Get flat list of currency options for single select dropdown.
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
List[dict]: List of currency option dictionaries
|
180
|
+
"""
|
181
|
+
provider_currencies = self.enabled_for_provider(provider_name).select_related(
|
182
|
+
'base_currency', 'network'
|
183
|
+
).order_by('is_popular', 'is_stable', 'base_currency__code', 'network__code')
|
184
|
+
|
185
|
+
options = []
|
186
|
+
for pc in provider_currencies:
|
187
|
+
# Create display name: "USDT (Ethereum)" or "BTC" for native currencies
|
188
|
+
if pc.network:
|
189
|
+
display_name = f"{pc.base_currency.code} ({pc.network.name})"
|
190
|
+
else:
|
191
|
+
display_name = pc.base_currency.code
|
192
|
+
|
193
|
+
# Get exchange rates using Currency manager
|
194
|
+
from ..models import Currency
|
195
|
+
usd_rate = Currency.objects.get_usd_rate(pc.base_currency.code)
|
196
|
+
tokens_per_usd = Currency.objects.get_tokens_per_usd(pc.base_currency.code)
|
197
|
+
|
198
|
+
option = {
|
199
|
+
'provider_currency_code': pc.provider_currency_code,
|
200
|
+
'display_name': display_name,
|
201
|
+
'base_currency_code': pc.base_currency.code,
|
202
|
+
'base_currency_name': pc.base_currency.name,
|
203
|
+
'network_code': pc.network.code if pc.network else None,
|
204
|
+
'network_name': pc.network.name if pc.network else None,
|
205
|
+
'currency_type': pc.base_currency.currency_type,
|
206
|
+
'is_popular': pc.is_popular,
|
207
|
+
'is_stable': pc.is_stable,
|
208
|
+
'available_for_payment': pc.available_for_payment,
|
209
|
+
'available_for_payout': pc.available_for_payout,
|
210
|
+
'min_amount': str(pc.min_amount) if pc.min_amount else None,
|
211
|
+
'max_amount': str(pc.max_amount) if pc.max_amount else None,
|
212
|
+
'logo_url': pc.logo_url,
|
213
|
+
# Exchange rates
|
214
|
+
'usd_rate': usd_rate,
|
215
|
+
'tokens_per_usd': tokens_per_usd
|
216
|
+
}
|
217
|
+
options.append(option)
|
218
|
+
|
219
|
+
# Sort: popular first, then stable, then alphabetically
|
220
|
+
def sort_key(option):
|
221
|
+
return (
|
222
|
+
0 if option['is_popular'] else 1, # Popular first
|
223
|
+
0 if option['is_stable'] else 1, # Then stable
|
224
|
+
option['base_currency_code'], # Then by base currency
|
225
|
+
option['network_name'] or '' # Then by network
|
226
|
+
)
|
227
|
+
|
228
|
+
options.sort(key=sort_key)
|
229
|
+
return options
|
76
230
|
|
77
|
-
def
|
78
|
-
"""
|
79
|
-
|
231
|
+
def get_usd_rates_for_provider(self, provider_name: str):
|
232
|
+
"""
|
233
|
+
Get USD exchange rates for all provider currencies.
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
dict: {provider_currency_code: {'rate': 0.0001, 'tokens_per_usd': 10000}}
|
237
|
+
"""
|
238
|
+
provider_currencies = self.enabled_for_provider(provider_name).select_related('base_currency')
|
239
|
+
rates = {}
|
240
|
+
|
241
|
+
for pc in provider_currencies:
|
242
|
+
try:
|
243
|
+
# Get rate: 1 BASE_CURRENCY = X USD
|
244
|
+
usd_rate = get_exchange_rate(pc.base_currency.code, 'USD')
|
245
|
+
|
246
|
+
# Calculate tokens per 1 USD
|
247
|
+
if usd_rate > 0:
|
248
|
+
tokens_per_usd = 1.0 / usd_rate
|
249
|
+
else:
|
250
|
+
tokens_per_usd = 0.0
|
251
|
+
|
252
|
+
rates[pc.provider_currency_code] = {
|
253
|
+
'usd_rate': round(usd_rate, 8),
|
254
|
+
'tokens_per_usd': round(tokens_per_usd, 2),
|
255
|
+
'base_currency': pc.base_currency.code,
|
256
|
+
'updated_at': timezone.now().isoformat()
|
257
|
+
}
|
258
|
+
|
259
|
+
except CurrencyError as e:
|
260
|
+
logger.warning(f"Failed to get rate for {pc.base_currency.code}: {e}")
|
261
|
+
rates[pc.provider_currency_code] = {
|
262
|
+
'usd_rate': 0.0,
|
263
|
+
'tokens_per_usd': 0.0,
|
264
|
+
'base_currency': pc.base_currency.code,
|
265
|
+
'error': str(e)
|
266
|
+
}
|
267
|
+
|
268
|
+
return rates
|
80
269
|
|
81
|
-
def
|
82
|
-
"""
|
83
|
-
|
270
|
+
def convert_amount(self, amount: float, from_currency_code: str, to_currency: str = 'USD'):
|
271
|
+
"""
|
272
|
+
Convert amount from provider currency to target currency.
|
273
|
+
|
274
|
+
Args:
|
275
|
+
amount: Amount to convert
|
276
|
+
from_currency_code: Provider currency code (e.g., 'USDTERC20')
|
277
|
+
to_currency: Target currency (default: 'USD')
|
278
|
+
|
279
|
+
Returns:
|
280
|
+
dict: {'amount': converted_amount, 'rate': exchange_rate, 'from': base_currency}
|
281
|
+
"""
|
282
|
+
try:
|
283
|
+
# Find provider currency and get base currency
|
284
|
+
pc = self.get(provider_currency_code=from_currency_code)
|
285
|
+
base_currency = pc.base_currency.code
|
286
|
+
|
287
|
+
# Convert via base currency
|
288
|
+
converted_amount = convert_currency(amount, base_currency, to_currency)
|
289
|
+
rate = get_exchange_rate(base_currency, to_currency)
|
290
|
+
|
291
|
+
return {
|
292
|
+
'amount': round(converted_amount, 2),
|
293
|
+
'rate': round(rate, 8),
|
294
|
+
'from': base_currency,
|
295
|
+
'to': to_currency,
|
296
|
+
'original_amount': amount,
|
297
|
+
'provider_code': from_currency_code
|
298
|
+
}
|
299
|
+
|
300
|
+
except (CurrencyError, self.model.DoesNotExist) as e:
|
301
|
+
logger.error(f"Conversion failed for {from_currency_code}: {e}")
|
302
|
+
return {
|
303
|
+
'amount': 0.0,
|
304
|
+
'rate': 0.0,
|
305
|
+
'error': str(e)
|
306
|
+
}
|
@@ -1,37 +1,143 @@
|
|
1
1
|
"""
|
2
|
-
Payment manager for UniversalPayment model.
|
2
|
+
Enhanced Payment manager for UniversalPayment model with query optimizations.
|
3
3
|
"""
|
4
4
|
|
5
5
|
from django.db import models
|
6
|
+
from django.utils import timezone
|
7
|
+
from datetime import timedelta
|
8
|
+
from django_cfg.modules.django_logger import get_logger
|
9
|
+
|
10
|
+
logger = get_logger("payment_manager")
|
11
|
+
|
12
|
+
|
13
|
+
class PaymentQuerySet(models.QuerySet):
|
14
|
+
"""Custom QuerySet for UniversalPayment with optimizations."""
|
15
|
+
|
16
|
+
def with_user(self):
|
17
|
+
"""Select related user to prevent N+1 queries."""
|
18
|
+
return self.select_related('user')
|
19
|
+
|
20
|
+
def with_events(self):
|
21
|
+
"""Skip prefetch for events since they use CharField payment_id, not ForeignKey."""
|
22
|
+
# PaymentEvent uses CharField payment_id, not ForeignKey, so no reverse relation exists
|
23
|
+
# Events should be fetched separately when needed
|
24
|
+
return self
|
25
|
+
|
26
|
+
def optimized(self):
|
27
|
+
"""Get optimized queryset for admin and API views."""
|
28
|
+
return self.select_related('user').with_events()
|
29
|
+
|
30
|
+
def active(self):
|
31
|
+
"""Get active payments (not failed, cancelled, or refunded)."""
|
32
|
+
return self.exclude(
|
33
|
+
status__in=['failed', 'cancelled', 'refunded', 'expired']
|
34
|
+
)
|
35
|
+
|
36
|
+
def completed(self):
|
37
|
+
"""Get only completed payments."""
|
38
|
+
return self.filter(status='completed')
|
39
|
+
|
40
|
+
def pending(self):
|
41
|
+
"""Get pending payments."""
|
42
|
+
return self.filter(status='pending')
|
43
|
+
|
44
|
+
def by_provider(self, provider):
|
45
|
+
"""Filter by payment provider."""
|
46
|
+
return self.filter(provider=provider)
|
47
|
+
|
48
|
+
def recent(self, days=30):
|
49
|
+
"""Get payments from last N days."""
|
50
|
+
cutoff_date = timezone.now() - timedelta(days=days)
|
51
|
+
return self.filter(created_at__gte=cutoff_date)
|
52
|
+
|
53
|
+
def by_amount_range(self, min_amount=None, max_amount=None):
|
54
|
+
"""Filter by USD amount range."""
|
55
|
+
queryset = self
|
56
|
+
if min_amount is not None:
|
57
|
+
queryset = queryset.filter(amount_usd__gte=min_amount)
|
58
|
+
if max_amount is not None:
|
59
|
+
queryset = queryset.filter(amount_usd__lte=max_amount)
|
60
|
+
return queryset
|
61
|
+
|
62
|
+
def by_user(self, user):
|
63
|
+
"""Filter by user."""
|
64
|
+
return self.filter(user=user)
|
65
|
+
|
66
|
+
def expired(self):
|
67
|
+
"""Get expired payments."""
|
68
|
+
return self.filter(
|
69
|
+
expires_at__lt=timezone.now(),
|
70
|
+
status__in=['pending', 'confirming']
|
71
|
+
)
|
6
72
|
|
7
73
|
|
8
74
|
class UniversalPaymentManager(models.Manager):
|
9
|
-
"""
|
75
|
+
"""Enhanced manager for UniversalPayment with optimization methods."""
|
76
|
+
|
77
|
+
def get_queryset(self):
|
78
|
+
"""Return custom QuerySet."""
|
79
|
+
return PaymentQuerySet(self.model, using=self._db)
|
80
|
+
|
81
|
+
def with_user(self):
|
82
|
+
"""Get payments with user data preloaded."""
|
83
|
+
return self.get_queryset().with_user()
|
84
|
+
|
85
|
+
def optimized(self):
|
86
|
+
"""Get optimized queryset for admin views."""
|
87
|
+
return self.get_queryset().optimized()
|
88
|
+
|
89
|
+
def active(self):
|
90
|
+
"""Get active payments."""
|
91
|
+
return self.get_queryset().active()
|
92
|
+
|
93
|
+
def completed(self):
|
94
|
+
"""Get completed payments."""
|
95
|
+
return self.get_queryset().completed()
|
96
|
+
|
97
|
+
def pending(self):
|
98
|
+
"""Get pending payments."""
|
99
|
+
return self.get_queryset().pending()
|
100
|
+
|
101
|
+
def recent(self, days=30):
|
102
|
+
"""Get recent payments."""
|
103
|
+
return self.get_queryset().recent(days)
|
10
104
|
|
11
|
-
def
|
12
|
-
"""
|
105
|
+
def by_provider(self, provider):
|
106
|
+
"""Get payments by provider."""
|
107
|
+
return self.get_queryset().by_provider(provider)
|
108
|
+
|
109
|
+
def create_payment(self, user, amount_usd: float, currency_code: str, provider: str, **kwargs):
|
110
|
+
"""Create a payment with automatic field generation."""
|
111
|
+
from uuid import uuid4
|
112
|
+
|
113
|
+
# Generate unique internal payment ID if not provided
|
114
|
+
internal_payment_id = kwargs.pop('internal_payment_id', f"PAY_{uuid4().hex[:8].upper()}")
|
115
|
+
|
13
116
|
payment = self.create(
|
14
117
|
user=user,
|
118
|
+
internal_payment_id=internal_payment_id,
|
15
119
|
amount_usd=amount_usd,
|
16
120
|
currency_code=currency_code.upper(),
|
17
121
|
provider=provider,
|
18
|
-
status=self.model.PaymentStatus.PENDING
|
122
|
+
status=self.model.PaymentStatus.PENDING,
|
123
|
+
**kwargs
|
19
124
|
)
|
125
|
+
|
20
126
|
return payment
|
21
127
|
|
22
128
|
def get_pending_payments(self, user=None):
|
23
129
|
"""Get pending payments for user or all users."""
|
24
|
-
queryset = self.
|
130
|
+
queryset = self.pending()
|
25
131
|
if user:
|
26
|
-
queryset = queryset.
|
27
|
-
return queryset
|
132
|
+
queryset = queryset.by_user(user)
|
133
|
+
return queryset.with_user()
|
28
134
|
|
29
135
|
def get_completed_payments(self, user=None):
|
30
136
|
"""Get completed payments for user or all users."""
|
31
|
-
queryset = self.
|
137
|
+
queryset = self.completed()
|
32
138
|
if user:
|
33
|
-
queryset = queryset.
|
34
|
-
return queryset
|
139
|
+
queryset = queryset.by_user(user)
|
140
|
+
return queryset.with_user()
|
35
141
|
|
36
142
|
def get_failed_payments(self, user=None):
|
37
143
|
"""Get failed/expired payments for user or all users."""
|
@@ -40,5 +146,47 @@ class UniversalPaymentManager(models.Manager):
|
|
40
146
|
self.model.PaymentStatus.EXPIRED
|
41
147
|
])
|
42
148
|
if user:
|
43
|
-
queryset = queryset.
|
44
|
-
return queryset
|
149
|
+
queryset = queryset.by_user(user)
|
150
|
+
return queryset.with_user()
|
151
|
+
|
152
|
+
def get_user_stats(self, user):
|
153
|
+
"""Get payment statistics for a user."""
|
154
|
+
user_payments = self.by_user(user)
|
155
|
+
|
156
|
+
return {
|
157
|
+
'total_payments': user_payments.count(),
|
158
|
+
'completed_payments': user_payments.completed().count(),
|
159
|
+
'pending_payments': user_payments.pending().count(),
|
160
|
+
'failed_payments': user_payments.filter(status='failed').count(),
|
161
|
+
'total_amount_usd': user_payments.completed().aggregate(
|
162
|
+
total=models.Sum('amount_usd')
|
163
|
+
)['total'] or 0,
|
164
|
+
'recent_payments_30d': user_payments.recent(30).count(),
|
165
|
+
}
|
166
|
+
|
167
|
+
def get_provider_stats(self, provider=None):
|
168
|
+
"""Get payment statistics by provider."""
|
169
|
+
if provider:
|
170
|
+
payments = self.by_provider(provider)
|
171
|
+
else:
|
172
|
+
payments = self.all()
|
173
|
+
|
174
|
+
return {
|
175
|
+
'total_payments': payments.count(),
|
176
|
+
'completed_payments': payments.completed().count(),
|
177
|
+
'pending_payments': payments.pending().count(),
|
178
|
+
'success_rate': (
|
179
|
+
payments.completed().count() / max(payments.count(), 1) * 100
|
180
|
+
),
|
181
|
+
'total_volume_usd': payments.completed().aggregate(
|
182
|
+
total=models.Sum('amount_usd')
|
183
|
+
)['total'] or 0,
|
184
|
+
}
|
185
|
+
|
186
|
+
def mark_expired_payments(self):
|
187
|
+
"""Mark expired payments as expired."""
|
188
|
+
expired_count = self.expired().update(
|
189
|
+
status=self.model.PaymentStatus.EXPIRED,
|
190
|
+
updated_at=timezone.now()
|
191
|
+
)
|
192
|
+
return expired_count
|
@@ -3,7 +3,7 @@ API Access Control Middleware.
|
|
3
3
|
Handles API key authentication and subscription validation.
|
4
4
|
"""
|
5
5
|
|
6
|
-
import
|
6
|
+
from django_cfg.modules.django_logger import get_logger
|
7
7
|
from typing import Optional, Tuple
|
8
8
|
from django.http import JsonResponse, HttpRequest, HttpResponse
|
9
9
|
from django.utils.deprecation import MiddlewareMixin
|
@@ -13,7 +13,7 @@ from ..models import APIKey, Subscription, EndpointGroup
|
|
13
13
|
from ..services import ApiKeyCache, RateLimitCache
|
14
14
|
from ..services.security import error_handler, SecurityError
|
15
15
|
|
16
|
-
logger =
|
16
|
+
logger = get_logger("api_access")
|
17
17
|
|
18
18
|
|
19
19
|
class APIAccessMiddleware(MiddlewareMixin):
|
@@ -3,16 +3,16 @@ Rate Limiting Middleware.
|
|
3
3
|
Implements sliding window rate limiting using Redis.
|
4
4
|
"""
|
5
5
|
|
6
|
-
import
|
6
|
+
from django_cfg.modules.django_logger import get_logger
|
7
7
|
import time
|
8
8
|
from typing import Optional
|
9
9
|
from django.http import JsonResponse, HttpRequest
|
10
10
|
from django.utils.deprecation import MiddlewareMixin
|
11
11
|
from django.conf import settings
|
12
12
|
from django.utils import timezone
|
13
|
-
from
|
13
|
+
from django.core.cache import cache
|
14
14
|
|
15
|
-
logger =
|
15
|
+
logger = get_logger("rate_limiting")
|
16
16
|
|
17
17
|
|
18
18
|
class RateLimitingMiddleware(MiddlewareMixin):
|
@@ -29,7 +29,6 @@ class RateLimitingMiddleware(MiddlewareMixin):
|
|
29
29
|
|
30
30
|
def __init__(self, get_response=None):
|
31
31
|
super().__init__(get_response)
|
32
|
-
self.rate_limit_cache = RateLimitCache()
|
33
32
|
|
34
33
|
# Default rate limits (can be overridden in settings)
|
35
34
|
self.default_limits = getattr(settings, 'PAYMENTS_RATE_LIMITS', {
|
@@ -142,14 +141,8 @@ class RateLimitingMiddleware(MiddlewareMixin):
|
|
142
141
|
# Get request count in window
|
143
142
|
redis_key = f"rate_limit:{rate_key}:{window}"
|
144
143
|
|
145
|
-
#
|
146
|
-
|
147
|
-
count = self.redis_service.sliding_window_count(
|
148
|
-
redis_key,
|
149
|
-
window_start,
|
150
|
-
current_time,
|
151
|
-
window_seconds
|
152
|
-
)
|
144
|
+
# Simple cache-based rate limiting
|
145
|
+
count = cache.get(redis_key, 0)
|
153
146
|
|
154
147
|
return count >= limit
|
155
148
|
|
@@ -181,12 +174,9 @@ class RateLimitingMiddleware(MiddlewareMixin):
|
|
181
174
|
if window_seconds:
|
182
175
|
redis_key = f"rate_limit:{rate_key}:{window}"
|
183
176
|
|
184
|
-
#
|
185
|
-
|
186
|
-
|
187
|
-
current_time,
|
188
|
-
window_seconds
|
189
|
-
)
|
177
|
+
# Increment request count
|
178
|
+
current_count = cache.get(redis_key, 0)
|
179
|
+
cache.set(redis_key, current_count + 1, window_seconds)
|
190
180
|
|
191
181
|
except Exception as e:
|
192
182
|
logger.error(f"Error recording request: {e}")
|