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,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 with convenient query methods."""
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 CurrencyNetworkManager(models.Manager):
71
- """Manager for CurrencyNetwork model."""
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 active(self):
74
- """Get only active networks."""
75
- return self.filter(is_active=True)
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 for_currency(self, currency_code: str):
78
- """Get networks for a specific currency."""
79
- return self.filter(currency__code__iexact=currency_code)
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 active_for_currency(self, currency_code: str):
82
- """Get active networks for a specific currency."""
83
- return self.active().filter(currency__code__iexact=currency_code)
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
- """Manager for UniversalPayment model."""
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 create_payment(self, user, amount_usd: float, currency_code: str, provider: str):
12
- """Create a new payment."""
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.filter(status=self.model.PaymentStatus.PENDING)
130
+ queryset = self.pending()
25
131
  if user:
26
- queryset = queryset.filter(user=user)
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.filter(status=self.model.PaymentStatus.COMPLETED)
137
+ queryset = self.completed()
32
138
  if user:
33
- queryset = queryset.filter(user=user)
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.filter(user=user)
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 logging
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 = logging.getLogger(__name__)
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 logging
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 ..services import RateLimitCache
13
+ from django.core.cache import cache
14
14
 
15
- logger = logging.getLogger(__name__)
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
- # Use Redis sorted set for sliding window
146
- # Remove old entries and count current entries
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
- # Add current timestamp to sorted set
185
- self.redis_service.record_request(
186
- redis_key,
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}")