django-cfg 1.2.31__py3-none-any.whl → 1.3.1__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 (256) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/health/views.py +4 -2
  3. django_cfg/apps/knowbase/config/settings.py +16 -15
  4. django_cfg/apps/payments/README.md +326 -0
  5. django_cfg/apps/payments/admin/__init__.py +20 -10
  6. django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
  7. django_cfg/apps/payments/admin/balance_admin.py +592 -297
  8. django_cfg/apps/payments/admin/currencies_admin.py +526 -222
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +465 -70
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
  39. django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
  43. django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +13 -18
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +172 -148
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -285
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +346 -467
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -481
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +234 -174
  74. django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
  75. django_cfg/apps/payments/services/providers/registry.py +367 -301
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +210 -129
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +128 -103
  85. django_cfg/apps/payments/signals/subscription_signals.py +194 -142
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +45 -48
  93. django_cfg/apps/payments/urls_admin.py +33 -42
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/config.py +1 -1
  110. django_cfg/core/config.py +40 -4
  111. django_cfg/core/generation.py +25 -4
  112. django_cfg/core/integration/README.md +363 -0
  113. django_cfg/core/integration/__init__.py +47 -0
  114. django_cfg/core/integration/commands_collector.py +239 -0
  115. django_cfg/core/integration/display/__init__.py +15 -0
  116. django_cfg/core/integration/display/base.py +157 -0
  117. django_cfg/core/integration/display/ngrok.py +164 -0
  118. django_cfg/core/integration/display/startup.py +815 -0
  119. django_cfg/core/integration/url_integration.py +123 -0
  120. django_cfg/core/integration/version_checker.py +160 -0
  121. django_cfg/management/commands/auto_generate.py +4 -0
  122. django_cfg/management/commands/check_settings.py +6 -0
  123. django_cfg/management/commands/clear_constance.py +5 -2
  124. django_cfg/management/commands/create_token.py +6 -0
  125. django_cfg/management/commands/list_urls.py +6 -0
  126. django_cfg/management/commands/migrate_all.py +6 -0
  127. django_cfg/management/commands/migrator.py +3 -0
  128. django_cfg/management/commands/rundramatiq.py +6 -0
  129. django_cfg/management/commands/runserver_ngrok.py +51 -29
  130. django_cfg/management/commands/script.py +6 -0
  131. django_cfg/management/commands/show_config.py +12 -2
  132. django_cfg/management/commands/show_urls.py +4 -0
  133. django_cfg/management/commands/superuser.py +6 -0
  134. django_cfg/management/commands/task_clear.py +4 -1
  135. django_cfg/management/commands/task_status.py +3 -1
  136. django_cfg/management/commands/test_email.py +3 -0
  137. django_cfg/management/commands/test_telegram.py +6 -0
  138. django_cfg/management/commands/test_twilio.py +6 -0
  139. django_cfg/management/commands/tree.py +6 -0
  140. django_cfg/management/commands/validate_config.py +155 -149
  141. django_cfg/models/constance.py +31 -11
  142. django_cfg/models/payments.py +175 -492
  143. django_cfg/modules/django_logger.py +160 -146
  144. django_cfg/modules/django_unfold/dashboard.py +64 -16
  145. django_cfg/registry/core.py +1 -0
  146. django_cfg/template_archive/django_sample.zip +0 -0
  147. django_cfg/utils/smart_defaults.py +222 -571
  148. django_cfg/utils/toolkit.py +51 -11
  149. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
  150. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
  151. django_cfg/apps/payments/__init__.py +0 -8
  152. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  153. django_cfg/apps/payments/config/module.py +0 -70
  154. django_cfg/apps/payments/config/providers.py +0 -105
  155. django_cfg/apps/payments/config/settings.py +0 -96
  156. django_cfg/apps/payments/config/utils.py +0 -52
  157. django_cfg/apps/payments/decorators.py +0 -291
  158. django_cfg/apps/payments/management/commands/README.md +0 -146
  159. django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
  160. django_cfg/apps/payments/managers/__init__.py +0 -23
  161. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  162. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  163. django_cfg/apps/payments/managers/currency_manager.py +0 -306
  164. django_cfg/apps/payments/managers/payment_manager.py +0 -192
  165. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  166. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  167. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
  168. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
  169. django_cfg/apps/payments/models/events.py +0 -73
  170. django_cfg/apps/payments/serializers/__init__.py +0 -57
  171. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  172. django_cfg/apps/payments/serializers/balance.py +0 -59
  173. django_cfg/apps/payments/serializers/currencies.py +0 -63
  174. django_cfg/apps/payments/serializers/payments.py +0 -62
  175. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  176. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  177. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  178. django_cfg/apps/payments/services/cache/base.py +0 -30
  179. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  180. django_cfg/apps/payments/services/internal_types.py +0 -461
  181. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  182. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  183. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
  184. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  185. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
  186. django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
  187. django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
  188. django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
  189. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
  190. django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
  191. django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
  192. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
  193. django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
  194. django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
  195. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
  196. django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
  197. django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
  198. django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
  199. django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
  200. django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
  201. django_cfg/apps/payments/services/security/__init__.py +0 -34
  202. django_cfg/apps/payments/services/security/error_handler.py +0 -635
  203. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  204. django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
  205. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  206. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  207. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  208. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  209. django_cfg/apps/payments/tasks/__init__.py +0 -12
  210. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  211. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
  212. django_cfg/apps/payments/templates/payments/base.html +0 -182
  213. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  214. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  215. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
  216. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  217. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
  218. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
  219. django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
  220. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
  221. django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
  222. django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
  223. django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
  224. django_cfg/apps/payments/templates/payments/stats.html +0 -261
  225. django_cfg/apps/payments/templates/payments/test.html +0 -213
  226. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  227. django_cfg/apps/payments/utils/__init__.py +0 -43
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -239
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -63
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -122
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -451
  241. django_cfg/apps/payments/views/templates/base.py +0 -212
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -158
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -244
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -66
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/template_archive/.gitignore +0 -1
  252. django_cfg/template_archive/__init__.py +0 -0
  253. django_cfg/urls.py +0 -33
  254. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  255. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  256. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,7 @@
1
1
  """
2
- Admin interface for currencies.
2
+ Currency Admin interfaces with Unfold integration.
3
+
4
+ Includes universal currency/rate update functionality and modern UI.
3
5
  """
4
6
 
5
7
  from django.contrib import admin
@@ -9,50 +11,92 @@ from django.contrib import messages
9
11
  from django.shortcuts import redirect
10
12
  from django.core.management import call_command
11
13
  from django.utils.safestring import mark_safe
12
- from unfold.admin import ModelAdmin
14
+ from django.db.models import Count, Q
15
+ from django.utils import timezone
16
+ from datetime import timedelta
17
+ import threading
18
+ from typing import Optional
19
+
20
+ from unfold.admin import ModelAdmin, TabularInline
13
21
  from unfold.decorators import display, action
14
22
  from unfold.enums import ActionVariant
15
- from unfold.admin import TabularInline
16
23
 
17
24
  from ..models import Currency, Network, ProviderCurrency
18
- from .filters import CurrencyTypeFilter
25
+ from .filters import CurrencyTypeFilter, CurrencyRateStatusFilter
26
+ from django_cfg.modules.django_logger import get_logger
27
+
28
+ logger = get_logger("currencies_admin")
19
29
 
20
30
 
21
31
  @admin.register(Currency)
22
32
  class CurrencyAdmin(ModelAdmin):
23
- """Admin interface for clean base currencies."""
33
+ """
34
+ Modern Currency admin with Unfold styling and universal update functionality.
24
35
 
25
- # Custom template to show statistics above listing
36
+ Features:
37
+ - Real-time USD rate display with freshness indicators
38
+ - Universal update button (populate + sync + rates)
39
+ - Advanced filtering and search
40
+ - Provider count statistics
41
+ - Integration with django_currency module
42
+ """
43
+
44
+ # Custom template for statistics dashboard
26
45
  change_list_template = 'admin/payments/currency/change_list.html'
27
46
 
28
47
  list_display = [
29
- 'code',
30
- 'name',
31
- 'currency_type',
48
+ 'code_display',
49
+ 'name_display',
50
+ 'currency_type_badge',
32
51
  'usd_rate_display',
33
- 'provider_count',
34
- 'created_at'
52
+ 'provider_count_badge',
53
+ 'rate_freshness',
54
+ 'created_at_display'
35
55
  ]
36
56
 
37
- list_display_links = ['code']
57
+ list_display_links = ['code_display']
38
58
 
39
- search_fields = ['code', 'name']
59
+ search_fields = [
60
+ 'code',
61
+ 'name',
62
+ 'symbol'
63
+ ]
40
64
 
41
65
  list_filter = [
42
- 'currency_type',
66
+ CurrencyTypeFilter,
67
+ CurrencyRateStatusFilter,
68
+ 'is_active',
43
69
  'created_at'
44
70
  ]
45
71
 
46
- readonly_fields = ['created_at', 'updated_at']
72
+ readonly_fields = [
73
+ 'created_at',
74
+ 'updated_at',
75
+ 'exchange_rate_source'
76
+ ]
47
77
 
48
- # Unfold action buttons above listing - only one universal button!
78
+ # Unfold actions
49
79
  actions_list = [
50
- 'universal_update_all'
80
+ 'universal_update_all',
81
+ 'update_selected_rates',
82
+ 'sync_provider_currencies'
51
83
  ]
52
84
 
53
85
  fieldsets = [
54
86
  ('Currency Information', {
55
- 'fields': ['code', 'name', 'currency_type']
87
+ 'fields': [
88
+ 'code',
89
+ 'name',
90
+ 'currency_type',
91
+ 'symbol',
92
+ 'decimal_places'
93
+ ]
94
+ }),
95
+ ('Status & Configuration', {
96
+ 'fields': [
97
+ 'is_active',
98
+ 'exchange_rate_source'
99
+ ]
56
100
  }),
57
101
  ('Timestamps', {
58
102
  'fields': ['created_at', 'updated_at'],
@@ -60,315 +104,575 @@ class CurrencyAdmin(ModelAdmin):
60
104
  })
61
105
  ]
62
106
 
63
- @display(description="USD Rate", ordering='usd_rate')
107
+ def get_queryset(self, request):
108
+ """Optimize queryset with provider count annotation."""
109
+ return super().get_queryset(request).annotate(
110
+ provider_count=Count('provider_configs')
111
+ ).select_related()
112
+
113
+ @display(description="Code", ordering='code')
114
+ def code_display(self, obj):
115
+ """Display currency code with symbol."""
116
+ if obj.symbol:
117
+ return format_html(
118
+ '<span class="font-mono font-bold text-primary-600 dark:text-primary-400">{}</span> '
119
+ '<span class="text-gray-500 text-sm">{}</span>',
120
+ obj.code,
121
+ obj.symbol
122
+ )
123
+ return format_html(
124
+ '<span class="font-mono font-bold text-primary-600 dark:text-primary-400">{}</span>',
125
+ obj.code
126
+ )
127
+
128
+ @display(description="Name", ordering='name')
129
+ def name_display(self, obj):
130
+ """Display currency name with truncation."""
131
+ if len(obj.name) > 25:
132
+ return format_html(
133
+ '<span title="{}">{}</span>',
134
+ obj.name,
135
+ obj.name[:22] + "..."
136
+ )
137
+ return obj.name
138
+
139
+ @display(description="Type", ordering='currency_type')
140
+ def currency_type_badge(self, obj):
141
+ """Display currency type with colored badge."""
142
+ if obj.currency_type == Currency.CurrencyType.FIAT:
143
+ return format_html(
144
+ '<span class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200">'
145
+ '💰 Fiat'
146
+ '</span>'
147
+ )
148
+ else:
149
+ return format_html(
150
+ '<span class="inline-flex items-center rounded-full bg-orange-100 px-2.5 py-0.5 text-xs font-medium text-orange-800 dark:bg-orange-900 dark:text-orange-200">'
151
+ '₿ Crypto'
152
+ '</span>'
153
+ )
154
+
155
+ @display(description="USD Rate", ordering='provider_configs__usd_rate')
64
156
  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
- )
157
+ """Display USD rate with freshness indicator."""
158
+ # Get the most recent rate from ProviderCurrency
159
+ provider_currency = obj.provider_configs_set.filter(
160
+ usd_rate__isnull=False
161
+ ).order_by('-updated_at').first()
162
+
163
+ if not provider_currency or not provider_currency.usd_rate:
164
+ return format_html(
165
+ '<span class="text-red-500 text-sm">❌ No rate</span>'
166
+ )
167
+
168
+ # Check freshness (24 hours)
169
+ is_fresh = (
170
+ provider_currency.updated_at and
171
+ timezone.now() - provider_currency.updated_at < timedelta(hours=24)
172
+ )
173
+
174
+ color_class = "text-green-600 dark:text-green-400" if is_fresh else "text-orange-600 dark:text-orange-400"
175
+ icon = "🟢" if is_fresh else "🟠"
176
+
177
+ if obj.currency_type == Currency.CurrencyType.FIAT:
178
+ # Fiat: show 1 USD = X CURRENCY
179
+ tokens_per_usd = 1.0 / float(provider_currency.usd_rate) if provider_currency.usd_rate > 0 else 0
180
+ return format_html(
181
+ '<div class="{}">{} $1 = {} {}</div>'
182
+ '<small class="text-xs text-gray-500">Updated: {}</small>',
183
+ color_class,
184
+ icon,
185
+ f"{tokens_per_usd:.4f}",
186
+ obj.code,
187
+ naturaltime(provider_currency.updated_at) if provider_currency.updated_at else "Never"
188
+ )
96
189
  else:
190
+ # Crypto: show 1 CURRENCY = X USD
191
+ usd_rate = float(provider_currency.usd_rate)
192
+ if usd_rate > 1:
193
+ rate_display = f"${usd_rate:,.2f}"
194
+ elif usd_rate > 0.01:
195
+ rate_display = f"${usd_rate:.4f}"
196
+ else:
197
+ rate_display = f"${usd_rate:.8f}"
198
+
97
199
  return format_html(
98
- '<span class="text-gray-500">❌ No rate</span><br><small class="text-xs text-gray-400">Never updated</small>'
200
+ '<div class="{}">{} 1 {} = {}</div>'
201
+ '<small class="text-xs text-gray-500">Updated: {}</small>',
202
+ color_class,
203
+ icon,
204
+ obj.code,
205
+ rate_display,
206
+ naturaltime(provider_currency.updated_at) if provider_currency.updated_at else "Never"
99
207
  )
100
208
 
101
209
  @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()
210
+ def provider_count_badge(self, obj):
211
+ """Display provider count with badge."""
212
+ count = getattr(obj, 'provider_count', 0)
105
213
  if count > 0:
106
214
  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
215
+ '<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200">'
216
+ '{} provider{}'
217
+ '</span>',
218
+ count,
219
+ 's' if count != 1 else ''
109
220
  )
110
221
  return format_html(
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>'
222
+ '<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-900 dark:text-gray-200">'
223
+ 'No providers'
224
+ '</span>'
112
225
  )
113
226
 
227
+ @display(description="Rate Status")
228
+ def rate_freshness(self, obj):
229
+ """Display rate freshness indicator."""
230
+ provider_currency = obj.provider_configs_set.filter(
231
+ usd_rate__isnull=False
232
+ ).order_by('-updated_at').first()
233
+
234
+ if not provider_currency or not provider_currency.updated_at:
235
+ return format_html('<span class="text-red-500">❌ Never</span>')
236
+
237
+ age = timezone.now() - provider_currency.updated_at
238
+
239
+ if age < timedelta(hours=1):
240
+ return format_html('<span class="text-green-500">🟢 Fresh</span>')
241
+ elif age < timedelta(hours=24):
242
+ return format_html('<span class="text-yellow-500">🟡 Recent</span>')
243
+ elif age < timedelta(days=7):
244
+ return format_html('<span class="text-orange-500">🟠 Stale</span>')
245
+ else:
246
+ return format_html('<span class="text-red-500">🔴 Old</span>')
247
+
248
+ @display(description="Created", ordering='created_at')
249
+ def created_at_display(self, obj):
250
+ """Display creation date."""
251
+ return naturaltime(obj.created_at)
252
+
114
253
  def changelist_view(self, request, extra_context=None):
115
- """Override changelist view to add default statistics."""
254
+ """Add statistics to changelist context."""
116
255
  extra_context = extra_context or {}
117
256
 
118
257
  try:
119
- from django.db.models import Count
120
-
121
- # Get statistics for template
258
+ # Basic statistics
122
259
  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()
260
+ fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
261
+ crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
262
+ active_count = Currency.objects.filter(is_active=True).count()
125
263
 
126
- # Count provider mappings
264
+ # Provider statistics
127
265
  total_provider_currencies = ProviderCurrency.objects.count()
128
266
  enabled_provider_currencies = ProviderCurrency.objects.filter(is_enabled=True).count()
129
267
 
130
- # Count currencies with USD rates
131
- currencies_with_rates = Currency.objects.filter(usd_rate__isnull=False).count()
268
+ # Rate statistics
269
+ currencies_with_rates = Currency.objects.filter(
270
+ provider_configs__usd_rate__isnull=False
271
+ ).distinct().count()
132
272
  rate_coverage = (currencies_with_rates / total_currencies * 100) if total_currencies > 0 else 0
133
273
 
134
- # Top popular currencies by provider count
274
+ # Fresh rates (updated in last 24 hours)
275
+ fresh_threshold = timezone.now() - timedelta(hours=24)
276
+ fresh_rates_count = Currency.objects.filter(
277
+ provider_configs__updated_at__gte=fresh_threshold
278
+ ).distinct().count()
279
+
280
+ # Top currencies by provider count
135
281
  top_currencies = Currency.objects.annotate(
136
- provider_count=Count('provider_mappings')
282
+ provider_count=Count('provider_configs')
137
283
  ).filter(provider_count__gt=0).order_by('-provider_count')[:5]
138
284
 
139
- # Pass data to template
140
285
  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,
286
+ 'currency_stats': {
287
+ 'total_currencies': total_currencies,
288
+ 'fiat_count': fiat_count,
289
+ 'crypto_count': crypto_count,
290
+ 'active_count': active_count,
291
+ 'total_provider_currencies': total_provider_currencies,
292
+ 'enabled_provider_currencies': enabled_provider_currencies,
293
+ 'currencies_with_rates': currencies_with_rates,
294
+ 'rate_coverage': rate_coverage,
295
+ 'fresh_rates_count': fresh_rates_count,
296
+ 'top_currencies': top_currencies,
297
+ }
149
298
  })
150
299
 
151
300
  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}")
301
+ logger.warning(f"Failed to generate currency statistics: {e}")
302
+ extra_context['currency_stats'] = None
156
303
 
157
304
  return super().changelist_view(request, extra_context)
158
305
 
159
-
160
- # Universal Admin Action - ONE BUTTON TO RULE THEM ALL!
306
+ # ===== ADMIN ACTIONS =====
161
307
 
162
308
  @action(
163
- description="🚀 Universal Update",
309
+ description="🚀 Universal Update (All)",
164
310
  icon="sync",
165
311
  variant=ActionVariant.SUCCESS,
166
312
  url_path="universal-update"
167
313
  )
168
314
  def universal_update_all(self, request):
169
- """Universal update: populate missing currencies + sync providers + update rates + show stats."""
315
+ """
316
+ Universal update: populate currencies + sync providers + update rates.
317
+
318
+ This is the main action that performs a complete system update.
319
+ """
170
320
  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
321
  def background_update():
177
- """Background task for full update."""
322
+ """Background task for comprehensive update."""
178
323
  try:
179
- # 1. Populate missing currencies (fast, skip if exists)
324
+ logger.info("Starting universal currency update")
325
+
326
+ # 1. Populate missing currencies (fast)
180
327
  call_command('manage_currencies', '--populate', '--skip-existing')
181
- sleep(1)
182
328
 
183
- # 2. Sync all providers (medium)
329
+ # 2. Sync all providers (medium speed)
184
330
  call_command('manage_providers', '--all')
185
- sleep(1)
186
331
 
187
- # 3. Update USD rates for all currencies (slower)
332
+ # 3. Update USD rates (slower)
188
333
  call_command('manage_currencies', '--rates-only')
189
334
 
335
+ logger.info("Universal currency update completed successfully")
336
+
190
337
  except Exception as e:
191
- print(f"Background universal update error: {e}")
338
+ logger.error(f"Universal update failed: {e}")
192
339
 
193
340
  # Start background update
194
341
  thread = threading.Thread(target=background_update)
195
342
  thread.daemon = True
196
343
  thread.start()
197
344
 
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()
345
+ # Generate immediate statistics for user feedback
346
+ stats = self._get_current_stats()
204
347
 
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]
348
+ success_message = self._generate_update_message(stats)
349
+ messages.success(request, mark_safe(success_message))
209
350
 
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>'
351
+ logger.info(f"Universal update initiated by user {request.user.username}")
213
352
 
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
- '''
353
+ except Exception as e:
354
+ error_msg = f" Failed to start universal update: {str(e)}"
355
+ messages.error(request, error_msg)
356
+ logger.error(f"Universal update initiation failed: {e}")
357
+
358
+ return redirect(request.META.get('HTTP_REFERER', '/admin/payments/currency/'))
359
+
360
+ @action(
361
+ description="💱 Update Selected Rates",
362
+ icon="trending_up",
363
+ variant=ActionVariant.WARNING
364
+ )
365
+ def update_selected_rates(self, request, queryset):
366
+ """Update USD rates for selected currencies only."""
367
+ try:
368
+ currency_codes = list(queryset.values_list('code', flat=True))
264
369
 
265
- messages.success(request, mark_safe(stats_and_status_html))
370
+ def background_rate_update():
371
+ """Background task for rate updates."""
372
+ try:
373
+ for code in currency_codes:
374
+ call_command('manage_currencies', '--currency', code, '--rates-only')
375
+ except Exception as e:
376
+ logger.error(f"Selected rate update failed: {e}")
377
+
378
+ thread = threading.Thread(target=background_rate_update)
379
+ thread.daemon = True
380
+ thread.start()
381
+
382
+ messages.success(
383
+ request,
384
+ f"💱 Started rate update for {len(currency_codes)} currencies: {', '.join(currency_codes[:5])}"
385
+ f"{'...' if len(currency_codes) > 5 else ''}"
386
+ )
266
387
 
267
388
  except Exception as e:
268
- messages.error(
269
- request,
270
- f"❌ Failed to start universal update: {str(e)}"
389
+ messages.error(request, f"❌ Failed to update rates: {str(e)}")
390
+
391
+ @action(
392
+ description="🔄 Sync Provider Currencies",
393
+ icon="cloud_sync",
394
+ variant=ActionVariant.INFO
395
+ )
396
+ def sync_provider_currencies(self, request, queryset):
397
+ """Sync provider currencies for selected base currencies."""
398
+ try:
399
+ currency_codes = list(queryset.values_list('code', flat=True))
400
+
401
+ def background_sync():
402
+ """Background task for provider sync."""
403
+ try:
404
+ call_command('manage_providers', '--all', '--currencies', ','.join(currency_codes))
405
+ except Exception as e:
406
+ logger.error(f"Provider sync failed: {e}")
407
+
408
+ thread = threading.Thread(target=background_sync)
409
+ thread.daemon = True
410
+ thread.start()
411
+
412
+ messages.success(
413
+ request,
414
+ f"🔄 Started provider sync for {len(currency_codes)} currencies"
271
415
  )
416
+
417
+ except Exception as e:
418
+ messages.error(request, f"❌ Failed to sync providers: {str(e)}")
419
+
420
+ # ===== HELPER METHODS =====
421
+
422
+ def _get_current_stats(self) -> dict:
423
+ """Get current system statistics."""
424
+ try:
425
+ return {
426
+ 'total_currencies': Currency.objects.count(),
427
+ 'fiat_count': Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count(),
428
+ 'crypto_count': Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count(),
429
+ 'total_provider_currencies': ProviderCurrency.objects.count(),
430
+ 'enabled_provider_currencies': ProviderCurrency.objects.filter(is_enabled=True).count(),
431
+ 'top_currencies': Currency.objects.annotate(
432
+ provider_count=Count('provider_configs')
433
+ ).filter(provider_count__gt=0).order_by('-provider_count')[:3]
434
+ }
435
+ except Exception as e:
436
+ logger.warning(f"Failed to get current stats: {e}")
437
+ return {}
438
+
439
+ def _generate_update_message(self, stats: dict) -> str:
440
+ """Generate HTML message for update status."""
441
+ top_currencies_html = ""
442
+ if 'top_currencies' in stats:
443
+ for currency in stats['top_currencies']:
444
+ provider_count = getattr(currency, 'provider_count', 0)
445
+ top_currencies_html += f'<li><strong>{currency.code}:</strong> {provider_count} providers</li>'
272
446
 
273
- return redirect(request.META.get('HTTP_REFERER', '/admin/django_cfg_payments/currency/'))
274
-
275
-
276
-
277
- # ===== NEW ADMIN CLASSES FOR NEW ARCHITECTURE =====
278
-
447
+ return f'''
448
+ <div class="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-4 rounded-lg border-l-4 border-green-500">
449
+ <h3 class="text-lg font-semibold text-green-800 dark:text-green-200 mb-3">🚀 Universal Update Started</h3>
450
+
451
+ <div class="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded-lg mb-3 border border-yellow-200 dark:border-yellow-700">
452
+ <p class="text-yellow-800 dark:text-yellow-200 font-medium">⏳ Background tasks running:</p>
453
+ <ul class="text-sm text-yellow-700 dark:text-yellow-300 mt-2 space-y-1">
454
+ <li>1️⃣ Populating missing currencies...</li>
455
+ <li>2️⃣ Syncing provider data...</li>
456
+ <li>3️⃣ Updating USD exchange rates...</li>
457
+ </ul>
458
+ <p class="text-xs text-yellow-600 dark:text-yellow-400 mt-2">💡 Refresh page in 2-3 minutes to see results</p>
459
+ </div>
460
+
461
+ <div class="grid grid-cols-3 gap-3 mb-3">
462
+ <div class="bg-white dark:bg-gray-800 p-3 rounded-lg border">
463
+ <span class="text-sm text-gray-600 dark:text-gray-400">Total Currencies</span>
464
+ <p class="text-xl font-bold text-gray-900 dark:text-gray-100">{stats.get('total_currencies', 0)}</p>
465
+ </div>
466
+ <div class="bg-white dark:bg-gray-800 p-3 rounded-lg border">
467
+ <span class="text-sm text-gray-600 dark:text-gray-400">Fiat / Crypto</span>
468
+ <p class="text-xl font-bold">
469
+ <span class="text-blue-600">{stats.get('fiat_count', 0)}</span> /
470
+ <span class="text-orange-600">{stats.get('crypto_count', 0)}</span>
471
+ </p>
472
+ </div>
473
+ <div class="bg-white dark:bg-gray-800 p-3 rounded-lg border">
474
+ <span class="text-sm text-gray-600 dark:text-gray-400">Provider Mappings</span>
475
+ <p class="text-xl font-bold text-green-600">{stats.get('enabled_provider_currencies', 0)}</p>
476
+ </div>
477
+ </div>
478
+
479
+ {f'<div class="bg-white dark:bg-gray-800 p-3 rounded-lg border"><h4 class="font-semibold mb-2">🚀 Top Currencies</h4><ul class="text-sm space-y-1">{top_currencies_html}</ul></div>' if top_currencies_html else ''}
480
+ </div>
481
+ '''
279
482
 
280
483
 
281
484
  @admin.register(Network)
282
485
  class NetworkAdmin(ModelAdmin):
283
- """Admin for blockchain networks."""
486
+ """Admin interface for blockchain networks."""
487
+
488
+ list_display = [
489
+ 'code_display',
490
+ 'name_display',
491
+ 'currency_count_badge',
492
+ 'created_at_display'
493
+ ]
494
+
495
+ search_fields = ['code', 'name']
284
496
 
285
- list_display = ["code", "name", "currency_count", "created_at"]
286
- search_fields = ["code", "name"]
497
+ readonly_fields = ['created_at', 'updated_at']
498
+
499
+ @display(description="Code", ordering='code')
500
+ def code_display(self, obj):
501
+ """Display network code with styling."""
502
+ return format_html(
503
+ '<span class="font-mono font-bold text-purple-600 dark:text-purple-400">{}</span>',
504
+ obj.code
505
+ )
506
+
507
+ @display(description="Name", ordering='name')
508
+ def name_display(self, obj):
509
+ """Display network name."""
510
+ return obj.name
287
511
 
288
512
  @display(description="Currencies")
289
- def currency_count(self, obj):
290
- """Show currency count."""
513
+ def currency_count_badge(self, obj):
514
+ """Display currency count for this network."""
291
515
  count = ProviderCurrency.objects.filter(network=obj).count()
292
- return f"{count} currencies"
516
+ if count > 0:
517
+ return format_html(
518
+ '<span class="inline-flex items-center rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-200">'
519
+ '{} currenc{}'
520
+ '</span>',
521
+ count,
522
+ 'ies' if count != 1 else 'y'
523
+ )
524
+ return format_html(
525
+ '<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-900 dark:text-gray-200">'
526
+ 'No currencies'
527
+ '</span>'
528
+ )
529
+
530
+ @display(description="Created", ordering='created_at')
531
+ def created_at_display(self, obj):
532
+ """Display creation date."""
533
+ return naturaltime(obj.created_at)
293
534
 
294
535
 
295
536
  @admin.register(ProviderCurrency)
296
537
  class ProviderCurrencyAdmin(ModelAdmin):
297
- """Admin for provider currencies."""
538
+ """Admin interface for provider-specific currency configurations."""
298
539
 
299
540
  list_display = [
300
- "provider_currency_code",
301
- "provider_name",
302
- "base_currency",
303
- "network",
304
- "usd_value_display",
305
- "status_badges"
541
+ 'provider_currency_code_display',
542
+ 'provider_name_badge',
543
+ 'base_currency_display',
544
+ 'network_display',
545
+ 'usd_value_display',
546
+ 'status_badges',
547
+ 'updated_at_display'
306
548
  ]
307
549
 
308
550
  list_filter = [
309
- "provider_name",
310
- "is_enabled",
311
- "is_popular",
312
- "is_stable"
551
+ 'provider',
552
+ 'is_enabled',
553
+ 'currency__currency_type',
554
+ 'network'
313
555
  ]
314
556
 
315
557
  search_fields = [
316
- "provider_currency_code",
317
- "base_currency__code"
558
+ 'provider_currency_code',
559
+ 'currency__code',
560
+ 'currency__name',
561
+ 'network__code'
562
+ ]
563
+
564
+ readonly_fields = [
565
+ 'created_at',
566
+ 'updated_at'
318
567
  ]
319
568
 
569
+ def get_queryset(self, request):
570
+ """Optimize queryset with related objects."""
571
+ return super().get_queryset(request).select_related(
572
+ 'currency', 'network'
573
+ )
574
+
575
+ @display(description="Provider Code", ordering='provider_currency_code')
576
+ def provider_currency_code_display(self, obj):
577
+ """Display provider-specific currency code."""
578
+ return format_html(
579
+ '<span class="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">{}</span>',
580
+ obj.provider_currency_code
581
+ )
582
+
583
+ @display(description="Provider", ordering='provider')
584
+ def provider_name_badge(self, obj):
585
+ """Display provider name with badge."""
586
+ color_map = {
587
+ 'nowpayments': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
588
+ 'cryptomus': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
589
+ 'cryptapi': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
590
+ }
591
+
592
+ color_class = color_map.get(obj.provider.lower(), 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200')
593
+
594
+ return format_html(
595
+ '<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {}">{}</span>',
596
+ color_class,
597
+ obj.provider.title()
598
+ )
599
+
600
+ @display(description="Currency", ordering='currency__code')
601
+ def base_currency_display(self, obj):
602
+ """Display base currency with type indicator."""
603
+ type_icon = "💰" if obj.currency.currency_type == Currency.CurrencyType.FIAT else "₿"
604
+ return format_html(
605
+ '{} <span class="font-bold">{}</span>',
606
+ type_icon,
607
+ obj.currency.code
608
+ )
609
+
610
+ @display(description="Network", ordering='network__code')
611
+ def network_display(self, obj):
612
+ """Display network information."""
613
+ if obj.network:
614
+ return format_html(
615
+ '<span class="text-purple-600 dark:text-purple-400">{}</span>',
616
+ obj.network.code
617
+ )
618
+ return format_html('<span class="text-gray-500">—</span>')
619
+
320
620
  @display(description="USD Value")
321
621
  def usd_value_display(self, obj):
322
- """Show USD value for this provider currency."""
622
+ """Display USD value with proper formatting."""
323
623
  try:
324
- usd_rate = obj.usd_rate
325
- tokens_per_usd = obj.tokens_per_usd
624
+ if not obj.usd_rate:
625
+ return format_html('<span class="text-gray-500">No rate</span>')
626
+
627
+ usd_rate = float(obj.usd_rate)
326
628
 
327
- if obj.base_currency.currency_type == 'fiat':
328
- # Fiat: show how many tokens for $1
629
+ if obj.currency.currency_type == Currency.CurrencyType.FIAT:
630
+ # Fiat: show tokens per USD
631
+ tokens_per_usd = 1.0 / usd_rate if usd_rate > 0 else 0
329
632
  return format_html(
330
633
  '<span class="text-blue-600 dark:text-blue-400">$1 = {} {}</span>',
331
634
  f"{tokens_per_usd:.4f}",
332
- obj.base_currency.code
635
+ obj.currency.code
333
636
  )
334
637
  else:
335
638
  # 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
- )
639
+ if usd_rate > 1000:
640
+ rate_display = f"${usd_rate:,.0f}"
641
+ elif usd_rate > 1:
642
+ rate_display = f"${usd_rate:,.2f}"
343
643
  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
- )
644
+ rate_display = f"${usd_rate:.4f}"
350
645
  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
- )
646
+ rate_display = f"${usd_rate:.8f}"
647
+
648
+ return format_html(
649
+ '<span class="text-green-600 dark:text-green-400">1 {} = {}</span>',
650
+ obj.currency.code,
651
+ rate_display
652
+ )
653
+
357
654
  except Exception as e:
358
655
  return format_html(
359
656
  '<span class="text-red-500">Error: {}</span>',
360
- str(e)[:20]
657
+ str(e)[:15]
361
658
  )
362
659
 
363
660
  @display(description="Status")
364
661
  def status_badges(self, obj):
365
662
  """Display status badges."""
366
663
  badges = []
664
+
367
665
  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
-
666
+ badges.append('<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200">✅ Enabled</span>')
667
+ else:
668
+ badges.append('<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900 dark:text-red-200">❌ Disabled</span>')
669
+
670
+ # Note: is_popular and is_stable fields don't exist in model
671
+ # These could be added later or calculated based on other criteria
672
+
673
+ return format_html(' '.join(badges))
674
+
675
+ @display(description="Updated", ordering='updated_at')
676
+ def updated_at_display(self, obj):
677
+ """Display last update time."""
678
+ return naturaltime(obj.updated_at)