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
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.2.27"
35
+ __version__ = "1.2.31"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -6,7 +6,7 @@ from .balance_admin import UserBalanceAdmin, TransactionAdmin
6
6
  from .payments_admin import UniversalPaymentAdmin
7
7
  from .subscriptions_admin import SubscriptionAdmin, EndpointGroupAdmin
8
8
  from .api_keys_admin import APIKeyAdmin
9
- from .currencies_admin import CurrencyAdmin, CurrencyNetworkAdmin
9
+ from .currencies_admin import CurrencyAdmin, NetworkAdmin, ProviderCurrencyAdmin
10
10
  from .tariffs_admin import TariffAdmin, TariffEndpointGroupAdmin
11
11
 
12
12
  __all__ = [
@@ -17,7 +17,8 @@ __all__ = [
17
17
  'EndpointGroupAdmin',
18
18
  'APIKeyAdmin',
19
19
  'CurrencyAdmin',
20
- 'CurrencyNetworkAdmin',
20
+ 'NetworkAdmin',
21
+ 'ProviderCurrencyAdmin',
21
22
  'TariffAdmin',
22
23
  'TariffEndpointGroupAdmin',
23
24
  ]
@@ -100,8 +100,8 @@ class UserBalanceAdmin(ModelAdmin):
100
100
  color = '#dc3545' # Red
101
101
 
102
102
  return format_html(
103
- '<span style="color: {}; font-weight: bold;">${:.2f}</span>',
104
- color, amount
103
+ '<span style="color: {}; font-weight: bold;">${}</span>',
104
+ color, f"{float(amount):.2f}"
105
105
  )
106
106
 
107
107
  @display(description="Reserved")
@@ -109,8 +109,8 @@ class UserBalanceAdmin(ModelAdmin):
109
109
  """Display reserved amount."""
110
110
  if obj.reserved_usd > 0:
111
111
  return format_html(
112
- '<span style="color: #6c757d;">${:.2f}</span>',
113
- obj.reserved_usd
112
+ '<span style="color: #6c757d;">${}</span>',
113
+ f"{float(obj.reserved_usd):.2f}"
114
114
  )
115
115
  return "—"
116
116
 
@@ -119,8 +119,8 @@ class UserBalanceAdmin(ModelAdmin):
119
119
  """Display available balance."""
120
120
  available = obj.amount_usd - obj.reserved_usd
121
121
  return format_html(
122
- '<span style="font-weight: bold;">${:.2f}</span>',
123
- available
122
+ '<span style="font-weight: bold;">${}</span>',
123
+ f"{float(available):.2f}"
124
124
  )
125
125
 
126
126
  @display(description="Last Transaction")
@@ -129,10 +129,10 @@ class UserBalanceAdmin(ModelAdmin):
129
129
  last_transaction = obj.user.transactions.order_by('-created_at').first()
130
130
  if last_transaction:
131
131
  return format_html(
132
- '<span style="color: {};">{} ${:.2f}</span><br><small>{}</small>',
132
+ '<span style="color: {};">{} ${}</span><br><small>{}</small>',
133
133
  '#28a745' if last_transaction.amount_usd > 0 else '#dc3545',
134
134
  '+' if last_transaction.amount_usd > 0 else '',
135
- abs(last_transaction.amount_usd),
135
+ f"{float(abs(last_transaction.amount_usd)):.2f}",
136
136
  naturaltime(last_transaction.created_at)
137
137
  )
138
138
  return "No transactions"
@@ -152,18 +152,18 @@ class UserBalanceAdmin(ModelAdmin):
152
152
  return format_html(
153
153
  '<div style="line-height: 1.6;">'
154
154
  '<strong>Statistics:</strong><br>'
155
- '• Total Credited: <span style="color: #28a745;">${:.2f}</span><br>'
156
- '• Total Debited: <span style="color: #dc3545;">${:.2f}</span><br>'
157
- '• Net Balance: <span style="color: {};">${:.2f}</span><br>'
155
+ '• Total Credited: <span style="color: #28a745;">${}</span><br>'
156
+ '• Total Debited: <span style="color: #dc3545;">${}</span><br>'
157
+ '• Net Balance: <span style="color: {};">${}</span><br>'
158
158
  '• Total Transactions: {}<br>'
159
- '• Available Balance: <strong>${:.2f}</strong>'
159
+ '• Available Balance: <strong>${}</strong>'
160
160
  '</div>',
161
- total_credited,
162
- total_debited,
161
+ f"{float(total_credited):.2f}",
162
+ f"{float(total_debited):.2f}",
163
163
  '#28a745' if (total_credited - total_debited) > 0 else '#dc3545',
164
- total_credited - total_debited,
164
+ f"{float(total_credited - total_debited):.2f}",
165
165
  transaction_count,
166
- obj.amount_usd - obj.reserved_usd
166
+ f"{float(obj.amount_usd - obj.reserved_usd):.2f}"
167
167
  )
168
168
 
169
169
  balance_statistics.short_description = "Balance Statistics"
@@ -381,7 +381,7 @@ class TransactionAdmin(ModelAdmin):
381
381
  '• ID: {}<br>'
382
382
  '• User: {} ({})<br>'
383
383
  '• Type: {}<br>'
384
- '• Amount: <span style="color: {};">${:.2f}</span><br>'
384
+ '• Amount: <span style="color: {};">${}</span><br>'
385
385
  '• Description: {}<br>'
386
386
  '• Created: {}<br>'
387
387
  '{}'
@@ -392,7 +392,7 @@ class TransactionAdmin(ModelAdmin):
392
392
  obj.user.email,
393
393
  obj.get_transaction_type_display(),
394
394
  '#28a745' if obj.amount_usd > 0 else '#dc3545',
395
- obj.amount_usd,
395
+ f"{float(obj.amount_usd):.2f}",
396
396
  obj.description,
397
397
  obj.created_at.strftime('%Y-%m-%d %H:%M:%S'),
398
398
  f'• Payment: {obj.payment.internal_payment_id}<br>' if obj.payment else '',
@@ -5,46 +5,54 @@ Admin interface for currencies.
5
5
  from django.contrib import admin
6
6
  from django.utils.html import format_html
7
7
  from django.contrib.humanize.templatetags.humanize import naturaltime
8
+ from django.contrib import messages
9
+ from django.shortcuts import redirect
10
+ from django.core.management import call_command
11
+ from django.utils.safestring import mark_safe
8
12
  from unfold.admin import ModelAdmin
9
- from unfold.decorators import display
13
+ from unfold.decorators import display, action
14
+ from unfold.enums import ActionVariant
15
+ from unfold.admin import TabularInline
10
16
 
11
- from ..models import Currency, CurrencyNetwork
17
+ from ..models import Currency, Network, ProviderCurrency
12
18
  from .filters import CurrencyTypeFilter
13
19
 
14
20
 
15
21
  @admin.register(Currency)
16
22
  class CurrencyAdmin(ModelAdmin):
17
- """Admin interface for currencies."""
23
+ """Admin interface for clean base currencies."""
24
+
25
+ # Custom template to show statistics above listing
26
+ change_list_template = 'admin/payments/currency/change_list.html'
18
27
 
19
28
  list_display = [
20
- 'currency_display',
21
- 'type_display',
22
- 'rate_display',
23
- 'status_display',
24
- 'created_at_display'
29
+ 'code',
30
+ 'name',
31
+ 'currency_type',
32
+ 'usd_rate_display',
33
+ 'provider_count',
34
+ 'created_at'
25
35
  ]
26
36
 
27
- list_display_links = ['currency_display']
37
+ list_display_links = ['code']
28
38
 
29
- search_fields = ['code', 'name', 'symbol']
39
+ search_fields = ['code', 'name']
30
40
 
31
41
  list_filter = [
32
- CurrencyTypeFilter,
33
- 'is_active',
42
+ 'currency_type',
34
43
  'created_at'
35
44
  ]
36
45
 
37
- readonly_fields = ['rate_updated_at', 'created_at', 'updated_at']
46
+ readonly_fields = ['created_at', 'updated_at']
47
+
48
+ # Unfold action buttons above listing - only one universal button!
49
+ actions_list = [
50
+ 'universal_update_all'
51
+ ]
38
52
 
39
53
  fieldsets = [
40
54
  ('Currency Information', {
41
- 'fields': ['code', 'name', 'symbol', 'currency_type']
42
- }),
43
- ('Configuration', {
44
- 'fields': ['decimal_places', 'min_payment_amount', 'is_active']
45
- }),
46
- ('Exchange Rate', {
47
- 'fields': ['usd_rate', 'rate_updated_at']
55
+ 'fields': ['code', 'name', 'currency_type']
48
56
  }),
49
57
  ('Timestamps', {
50
58
  'fields': ['created_at', 'updated_at'],
@@ -52,135 +60,315 @@ class CurrencyAdmin(ModelAdmin):
52
60
  })
53
61
  ]
54
62
 
55
- @display(description="Currency")
56
- def currency_display(self, obj):
57
- """Display currency with symbol."""
63
+ @display(description="USD Rate", ordering='usd_rate')
64
+ def usd_rate_display(self, obj):
65
+ """Show USD exchange rate with cache status."""
66
+ if obj.usd_rate and obj.rate_updated_at:
67
+ # Check if rate is fresh (less than 24 hours)
68
+ from django.utils import timezone
69
+ from datetime import timedelta
70
+
71
+ is_fresh = timezone.now() - obj.rate_updated_at < timedelta(hours=24)
72
+ color_class = "text-green-600 dark:text-green-400" if is_fresh else "text-orange-600 dark:text-orange-400"
73
+ icon = "🟢" if is_fresh else "🟠"
74
+
75
+ if obj.currency_type == 'fiat':
76
+ # Fiat currencies show as 1 USD = X CURRENCY
77
+ tokens_per_usd = 1.0 / float(obj.usd_rate) if obj.usd_rate > 0 else 0
78
+ return format_html(
79
+ '<span class="{}">{} $1 = {} {}</span><br><small class="text-xs text-gray-500">Updated: {}</small>',
80
+ color_class,
81
+ icon,
82
+ f"{tokens_per_usd:.4f}",
83
+ obj.code,
84
+ naturaltime(obj.rate_updated_at)
85
+ )
86
+ else:
87
+ # Crypto currencies show as 1 CURRENCY = X USD
88
+ return format_html(
89
+ '<span class="{}">{} 1 {} = ${}</span><br><small class="text-xs text-gray-500">Updated: {}</small>',
90
+ color_class,
91
+ icon,
92
+ obj.code,
93
+ f"{float(obj.usd_rate):.8f}",
94
+ naturaltime(obj.rate_updated_at)
95
+ )
96
+ else:
97
+ return format_html(
98
+ '<span class="text-gray-500">❌ No rate</span><br><small class="text-xs text-gray-400">Never updated</small>'
99
+ )
100
+
101
+ @display(description="Providers")
102
+ def provider_count(self, obj):
103
+ """Show how many providers support this currency."""
104
+ count = getattr(obj, 'provider_mappings', obj.provider_currency_set if hasattr(obj, 'provider_currency_set') else []).count()
105
+ if count > 0:
106
+ return format_html(
107
+ '<span class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">{} providers</span>',
108
+ count
109
+ )
58
110
  return format_html(
59
- '<strong>{}</strong> {}<br><small>{}</small>',
60
- obj.code,
61
- obj.symbol,
62
- obj.name
111
+ '<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800">No providers</span>'
63
112
  )
64
113
 
65
- @display(description="Type")
66
- def type_display(self, obj):
67
- """Display currency type with badge."""
68
- type_colors = {
69
- 'fiat': '#28a745',
70
- 'crypto': '#fd7e14',
71
- }
114
+ def changelist_view(self, request, extra_context=None):
115
+ """Override changelist view to add default statistics."""
116
+ extra_context = extra_context or {}
72
117
 
73
- color = type_colors.get(obj.currency_type, '#6c757d')
118
+ try:
119
+ from django.db.models import Count
120
+
121
+ # Get statistics for template
122
+ total_currencies = Currency.objects.count()
123
+ fiat_count = Currency.objects.filter(currency_type='fiat').count()
124
+ crypto_count = Currency.objects.filter(currency_type='crypto').count()
125
+
126
+ # Count provider mappings
127
+ total_provider_currencies = ProviderCurrency.objects.count()
128
+ enabled_provider_currencies = ProviderCurrency.objects.filter(is_enabled=True).count()
129
+
130
+ # Count currencies with USD rates
131
+ currencies_with_rates = Currency.objects.filter(usd_rate__isnull=False).count()
132
+ rate_coverage = (currencies_with_rates / total_currencies * 100) if total_currencies > 0 else 0
133
+
134
+ # Top popular currencies by provider count
135
+ top_currencies = Currency.objects.annotate(
136
+ provider_count=Count('provider_mappings')
137
+ ).filter(provider_count__gt=0).order_by('-provider_count')[:5]
138
+
139
+ # Pass data to template
140
+ extra_context.update({
141
+ 'total_currencies': total_currencies,
142
+ 'fiat_count': fiat_count,
143
+ 'crypto_count': crypto_count,
144
+ 'total_provider_currencies': total_provider_currencies,
145
+ 'enabled_provider_currencies': enabled_provider_currencies,
146
+ 'currencies_with_rates': currencies_with_rates,
147
+ 'rate_coverage': rate_coverage,
148
+ 'top_currencies': top_currencies,
149
+ })
150
+
151
+ except Exception as e:
152
+ # If stats fail, just log and continue
153
+ import logging
154
+ logger = logging.getLogger(__name__)
155
+ logger.warning(f"Failed to generate currency stats: {e}")
74
156
 
75
- return format_html(
76
- '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>',
77
- color,
78
- obj.get_currency_type_display()
79
- )
157
+ return super().changelist_view(request, extra_context)
80
158
 
81
- @display(description="USD Rate")
82
- def rate_display(self, obj):
83
- """Display exchange rate."""
84
- if obj.usd_rate != 1.0:
85
- return format_html(
86
- '<strong>1 {} = ${:.6f}</strong><br><small>Updated: {}</small>',
87
- obj.code,
88
- obj.usd_rate,
89
- naturaltime(obj.rate_updated_at) if obj.rate_updated_at else 'Never'
90
- )
91
- return format_html('<span style="color: #6c757d;">Base currency</span>')
92
159
 
93
- @display(description="Status")
94
- def status_display(self, obj):
95
- """Display status badge."""
96
- if obj.is_active:
97
- return format_html(
98
- '<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Active</span>'
99
- )
100
- else:
101
- return format_html(
102
- '<span style="background: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Inactive</span>'
160
+ # Universal Admin Action - ONE BUTTON TO RULE THEM ALL!
161
+
162
+ @action(
163
+ description="🚀 Universal Update",
164
+ icon="sync",
165
+ variant=ActionVariant.SUCCESS,
166
+ url_path="universal-update"
167
+ )
168
+ def universal_update_all(self, request):
169
+ """Universal update: populate missing currencies + sync providers + update rates + show stats."""
170
+ try:
171
+ import threading
172
+ from django.core.management import call_command
173
+ from django.db.models import Count
174
+ from time import sleep
175
+
176
+ def background_update():
177
+ """Background task for full update."""
178
+ try:
179
+ # 1. Populate missing currencies (fast, skip if exists)
180
+ call_command('manage_currencies', '--populate', '--skip-existing')
181
+ sleep(1)
182
+
183
+ # 2. Sync all providers (medium)
184
+ call_command('manage_providers', '--all')
185
+ sleep(1)
186
+
187
+ # 3. Update USD rates for all currencies (slower)
188
+ call_command('manage_currencies', '--rates-only')
189
+
190
+ except Exception as e:
191
+ print(f"Background universal update error: {e}")
192
+
193
+ # Start background update
194
+ thread = threading.Thread(target=background_update)
195
+ thread.daemon = True
196
+ thread.start()
197
+
198
+ # Show immediate stats while update is running
199
+ total_currencies = Currency.objects.count()
200
+ fiat_count = Currency.objects.filter(currency_type='fiat').count()
201
+ crypto_count = Currency.objects.filter(currency_type='crypto').count()
202
+ total_provider_currencies = ProviderCurrency.objects.count()
203
+ enabled_provider_currencies = ProviderCurrency.objects.filter(is_enabled=True).count()
204
+
205
+ # Top popular currencies by provider count
206
+ top_currencies = Currency.objects.annotate(
207
+ provider_count=Count('provider_mappings')
208
+ ).filter(provider_count__gt=0).order_by('-provider_count')[:5]
209
+
210
+ currency_list = ""
211
+ for currency in top_currencies:
212
+ currency_list += f'<li class="text-font-default-light dark:text-font-default-dark"><span class="font-semibold text-primary-600 dark:text-primary-500">{currency.code}:</span> {currency.provider_count} providers</li>'
213
+
214
+ stats_and_status_html = f'''
215
+ <div class="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-5 rounded-default border-l-4 border-green-500 mt-3">
216
+ <h3 class="text-lg font-semibold text-font-important-light dark:text-font-important-dark mb-4">🚀 Universal Update Started</h3>
217
+
218
+ <div class="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded-default mb-4 border border-yellow-200 dark:border-yellow-700">
219
+ <p class="text-yellow-800 dark:text-yellow-200 font-medium">⏳ Background tasks running:</p>
220
+ <ul class="text-sm text-yellow-700 dark:text-yellow-300 mt-2 space-y-1">
221
+ <li>1️⃣ Populating missing currencies...</li>
222
+ <li>2️⃣ Syncing provider data...</li>
223
+ <li>3️⃣ Updating USD exchange rates...</li>
224
+ </ul>
225
+ <p class="text-xs text-yellow-600 dark:text-yellow-400 mt-2">💡 Refresh page in 2-3 minutes to see results</p>
226
+ </div>
227
+
228
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-3">📊 Current Statistics</h4>
229
+
230
+ <div class="grid grid-cols-2 gap-4 mb-4">
231
+ <div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-3 rounded-default">
232
+ <span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">Total currencies</span>
233
+ <p class="text-xl font-bold text-font-important-light dark:text-font-important-dark">{total_currencies}</p>
234
+ </div>
235
+ <div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-3 rounded-default">
236
+ <span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">Provider Mappings</span>
237
+ <p class="text-xl font-bold">
238
+ <span class="text-green-600 dark:text-green-400">{enabled_provider_currencies}</span>
239
+ <span class="text-font-subtle-light dark:text-font-subtle-dark mx-1">/</span>
240
+ <span class="text-gray-600 dark:text-gray-400">{total_provider_currencies}</span>
241
+ </p>
242
+ </div>
243
+ </div>
244
+
245
+ <div class="grid grid-cols-2 gap-4 mb-4">
246
+ <div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-3 rounded-default">
247
+ <span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">Fiat currencies</span>
248
+ <p class="text-xl font-bold text-blue-600 dark:text-blue-400">{fiat_count}</p>
249
+ </div>
250
+ <div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-3 rounded-default">
251
+ <span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">Cryptocurrencies</span>
252
+ <p class="text-xl font-bold text-orange-600 dark:text-orange-400">{crypto_count}</p>
253
+ </div>
254
+ </div>
255
+
256
+ <div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-3 rounded-default">
257
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">🚀 Most Supported Currencies</h4>
258
+ <ul class="space-y-1 text-sm">
259
+ {currency_list}
260
+ </ul>
261
+ </div>
262
+ </div>
263
+ '''
264
+
265
+ messages.success(request, mark_safe(stats_and_status_html))
266
+
267
+ except Exception as e:
268
+ messages.error(
269
+ request,
270
+ f"❌ Failed to start universal update: {str(e)}"
103
271
  )
272
+
273
+ return redirect(request.META.get('HTTP_REFERER', '/admin/django_cfg_payments/currency/'))
274
+
275
+
276
+
277
+ # ===== NEW ADMIN CLASSES FOR NEW ARCHITECTURE =====
278
+
279
+
280
+
281
+ @admin.register(Network)
282
+ class NetworkAdmin(ModelAdmin):
283
+ """Admin for blockchain networks."""
104
284
 
105
- @display(description="Created")
106
- def created_at_display(self, obj):
107
- """Display creation date."""
108
- return naturaltime(obj.created_at)
285
+ list_display = ["code", "name", "currency_count", "created_at"]
286
+ search_fields = ["code", "name"]
287
+
288
+ @display(description="Currencies")
289
+ def currency_count(self, obj):
290
+ """Show currency count."""
291
+ count = ProviderCurrency.objects.filter(network=obj).count()
292
+ return f"{count} currencies"
109
293
 
110
294
 
111
- @admin.register(CurrencyNetwork)
112
- class CurrencyNetworkAdmin(ModelAdmin):
113
- """Admin interface for currency networks."""
295
+ @admin.register(ProviderCurrency)
296
+ class ProviderCurrencyAdmin(ModelAdmin):
297
+ """Admin for provider currencies."""
114
298
 
115
299
  list_display = [
116
- 'network_display',
117
- 'currency_display',
118
- 'status_display',
119
- 'confirmations_display',
120
- 'created_at_display'
300
+ "provider_currency_code",
301
+ "provider_name",
302
+ "base_currency",
303
+ "network",
304
+ "usd_value_display",
305
+ "status_badges"
121
306
  ]
122
307
 
123
- list_display_links = ['network_display']
124
-
125
- search_fields = ['network_name', 'network_code', 'currency__code', 'currency__name']
126
-
127
- list_filter = ['currency', 'is_active', 'created_at']
128
-
129
- readonly_fields = ['created_at', 'updated_at']
130
-
131
- fieldsets = [
132
- ('Network Information', {
133
- 'fields': ['currency', 'network_name', 'network_code']
134
- }),
135
- ('Configuration', {
136
- 'fields': ['confirmation_blocks', 'is_active']
137
- }),
138
- ('Timestamps', {
139
- 'fields': ['created_at', 'updated_at'],
140
- 'classes': ['collapse']
141
- })
308
+ list_filter = [
309
+ "provider_name",
310
+ "is_enabled",
311
+ "is_popular",
312
+ "is_stable"
142
313
  ]
143
314
 
144
- @display(description="Network")
145
- def network_display(self, obj):
146
- """Display network information."""
147
- return format_html(
148
- '<strong>{}</strong><br><small>{}</small>',
149
- obj.network_name,
150
- obj.network_code
151
- )
152
-
153
- @display(description="Currency")
154
- def currency_display(self, obj):
155
- """Display currency information."""
156
- return format_html(
157
- '<strong>{}</strong> {}<br><small>{}</small>',
158
- obj.currency.code,
159
- obj.currency.symbol,
160
- obj.currency.name
161
- )
315
+ search_fields = [
316
+ "provider_currency_code",
317
+ "base_currency__code"
318
+ ]
162
319
 
163
- @display(description="Status")
164
- def status_display(self, obj):
165
- """Display status badge."""
166
- if obj.is_active:
167
- return format_html(
168
- '<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Active</span>'
169
- )
170
- else:
320
+ @display(description="USD Value")
321
+ def usd_value_display(self, obj):
322
+ """Show USD value for this provider currency."""
323
+ try:
324
+ usd_rate = obj.usd_rate
325
+ tokens_per_usd = obj.tokens_per_usd
326
+
327
+ if obj.base_currency.currency_type == 'fiat':
328
+ # Fiat: show how many tokens for $1
329
+ return format_html(
330
+ '<span class="text-blue-600 dark:text-blue-400">$1 = {} {}</span>',
331
+ f"{tokens_per_usd:.4f}",
332
+ obj.base_currency.code
333
+ )
334
+ else:
335
+ # Crypto: show USD value
336
+ if usd_rate > 1:
337
+ # High value crypto (like BTC)
338
+ return format_html(
339
+ '<span class="text-green-600 dark:text-green-400 font-semibold">1 {} = ${}</span>',
340
+ obj.base_currency.code,
341
+ f"{usd_rate:,.2f}"
342
+ )
343
+ elif usd_rate > 0.01:
344
+ # Medium value crypto
345
+ return format_html(
346
+ '<span class="text-green-600 dark:text-green-400">1 {} = ${}</span>',
347
+ obj.base_currency.code,
348
+ f"{usd_rate:.4f}"
349
+ )
350
+ else:
351
+ # Low value crypto (show more decimals)
352
+ return format_html(
353
+ '<span class="text-green-600 dark:text-green-400">1 {} = ${}</span>',
354
+ obj.base_currency.code,
355
+ f"{usd_rate:.8f}"
356
+ )
357
+ except Exception as e:
171
358
  return format_html(
172
- '<span style="background: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Inactive</span>'
359
+ '<span class="text-red-500">Error: {}</span>',
360
+ str(e)[:20]
173
361
  )
174
362
 
175
- @display(description="Confirmations")
176
- def confirmations_display(self, obj):
177
- """Display confirmation blocks."""
178
- return format_html(
179
- '<span style="font-weight: bold;">{}</span> blocks',
180
- obj.confirmation_blocks
181
- )
182
-
183
- @display(description="Created")
184
- def created_at_display(self, obj):
185
- """Display creation date."""
186
- return naturaltime(obj.created_at)
363
+ @display(description="Status")
364
+ def status_badges(self, obj):
365
+ """Display status badges."""
366
+ badges = []
367
+ if obj.is_enabled:
368
+ badges.append("✅ Enabled")
369
+ if obj.is_popular:
370
+ badges.append("⭐ Popular")
371
+ if obj.is_stable:
372
+ badges.append("🔒 Stable")
373
+ return " | ".join(badges) if badges else "❌ Disabled"
374
+