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,162 +1,511 @@
1
1
  """
2
- Admin interface for subscriptions.
2
+ Subscription Admin interfaces with Unfold integration.
3
+
4
+ Advanced subscription lifecycle management and monitoring.
3
5
  """
4
6
 
5
7
  from django.contrib import admin
6
8
  from django.utils.html import format_html
7
9
  from django.contrib.humanize.templatetags.humanize import naturaltime
8
- from unfold.admin import ModelAdmin
9
- from unfold.decorators import display
10
+ from django.contrib import messages
11
+ from django.shortcuts import redirect
12
+ from django.utils.safestring import mark_safe
13
+ from django.db.models import Count, Sum, Q
14
+ from django.utils import timezone
15
+ from datetime import timedelta
16
+ from typing import Optional
17
+
18
+ from unfold.admin import ModelAdmin, TabularInline
19
+ from unfold.decorators import display, action
20
+ from unfold.enums import ActionVariant
21
+
22
+ from ..models import Subscription, EndpointGroup, Tariff, TariffEndpointGroup
23
+ from .filters import SubscriptionTierFilter, SubscriptionStatusFilter, RecentActivityFilter
24
+ from django_cfg.modules.django_logger import get_logger
25
+
26
+ logger = get_logger("subscriptions_admin")
10
27
 
11
- from ..models import Subscription, EndpointGroup
12
- from .filters import SubscriptionStatusFilter, SubscriptionTierFilter, UsageExceededFilter, UserEmailFilter
28
+
29
+ class TariffEndpointGroupInline(TabularInline):
30
+ """Inline for tariff endpoint groups."""
31
+ model = TariffEndpointGroup
32
+ extra = 0
33
+ fields = ['endpoint_group', 'custom_rate_limit', 'is_enabled']
13
34
 
14
35
 
15
36
  @admin.register(Subscription)
16
37
  class SubscriptionAdmin(ModelAdmin):
17
- """Admin interface for subscriptions."""
38
+ """
39
+ Advanced Subscription admin with lifecycle management.
40
+
41
+ Features:
42
+ - Subscription lifecycle tracking
43
+ - Usage monitoring and alerts
44
+ - Bulk subscription operations
45
+ - Expiration management
46
+ - Tier-based filtering and actions
47
+ """
48
+
49
+ # Custom template for subscription statistics
50
+ change_list_template = 'admin/payments/subscription/change_list.html'
18
51
 
19
52
  list_display = [
20
53
  'subscription_display',
21
54
  'user_display',
22
- 'endpoint_group_display',
23
55
  'tier_display',
24
56
  'status_display',
25
57
  'usage_display',
26
- 'expires_display'
58
+ 'expiry_display',
59
+ 'created_at_display'
27
60
  ]
28
61
 
29
62
  list_display_links = ['subscription_display']
30
63
 
31
64
  search_fields = [
65
+ 'id',
32
66
  'user__email',
33
- 'endpoint_group__name',
34
- 'endpoint_group__display_name'
67
+ 'user__username',
68
+ 'tier'
35
69
  ]
36
70
 
37
71
  list_filter = [
38
72
  SubscriptionStatusFilter,
39
73
  SubscriptionTierFilter,
40
- UsageExceededFilter,
41
- UserEmailFilter,
42
- 'endpoint_group',
43
- 'created_at'
74
+ RecentActivityFilter,
75
+ 'created_at',
76
+ 'expires_at'
44
77
  ]
45
78
 
46
79
  readonly_fields = [
80
+ 'id',
47
81
  'created_at',
48
- 'updated_at'
82
+ 'updated_at',
83
+ 'last_request_at'
84
+ ]
85
+
86
+ # Unfold actions
87
+ actions_list = [
88
+ 'activate_subscriptions',
89
+ 'suspend_subscriptions',
90
+ 'extend_subscriptions',
49
91
  ]
50
92
 
51
93
  fieldsets = [
52
94
  ('Subscription Information', {
53
- 'fields': ['user', 'endpoint_group', 'tier', 'status']
95
+ 'fields': [
96
+ 'id',
97
+ 'user',
98
+ 'tier',
99
+ 'status'
100
+ ]
54
101
  }),
55
- ('Billing', {
56
- 'fields': ['monthly_price', 'last_billed', 'next_billing']
102
+ ('Usage & Limits', {
103
+ 'fields': [
104
+ 'total_requests',
105
+ 'requests_per_hour',
106
+ 'requests_per_day',
107
+ 'last_request_at'
108
+ ]
57
109
  }),
58
- ('Usage', {
59
- 'fields': ['usage_limit', 'usage_current']
110
+ ('Billing & Expiry', {
111
+ 'fields': [
112
+ 'monthly_cost_usd',
113
+ 'starts_at',
114
+ 'expires_at',
115
+ 'auto_renew'
116
+ ]
60
117
  }),
61
- ('Dates', {
62
- 'fields': ['expires_at', 'cancelled_at', 'created_at', 'updated_at'],
118
+ ('Timestamps', {
119
+ 'fields': ['created_at', 'updated_at'],
63
120
  'classes': ['collapse']
64
121
  })
65
122
  ]
66
123
 
67
- @display(description="Subscription")
124
+ def get_queryset(self, request):
125
+ """Optimize queryset with related data."""
126
+ return super().get_queryset(request).select_related('user').prefetch_related('endpoint_groups')
127
+
128
+ @display(description="Subscription", ordering='id')
68
129
  def subscription_display(self, obj):
69
- """Display subscription info."""
130
+ """Display subscription ID with tier indicator."""
131
+ short_id = str(obj.id)[:8]
132
+
133
+ tier_icons = {
134
+ 'free': '🆓',
135
+ 'basic': '🥉',
136
+ 'pro': '🥈',
137
+ 'enterprise': '🥇'
138
+ }
139
+
140
+ tier_icon = tier_icons.get(obj.tier, '📋')
141
+
70
142
  return format_html(
71
- '<strong>#{}</strong><br><small>{}</small>',
72
- str(obj.id)[:8],
73
- obj.endpoint_group.display_name
143
+ '<div class="flex items-center space-x-2">'
144
+ '<span class="text-lg">{}</span>'
145
+ '<span class="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded" title="Full ID: {}">{}</span>'
146
+ '</div>',
147
+ tier_icon,
148
+ obj.id,
149
+ short_id
74
150
  )
75
151
 
76
- @display(description="User")
152
+ @display(description="User", ordering='user__email')
77
153
  def user_display(self, obj):
78
- """Display user information."""
79
- return format_html(
80
- '<strong>{}</strong><br><small>{}</small>',
81
- obj.user.get_full_name() or obj.user.email,
82
- obj.user.email
83
- )
154
+ """Display user information with subscription history."""
155
+ if obj.user:
156
+ # Count user's total subscriptions
157
+ total_subscriptions = Subscription.objects.filter(user=obj.user).count()
158
+
159
+ return format_html(
160
+ '<div>'
161
+ '<div class="font-medium text-gray-900 dark:text-gray-100">{}</div>'
162
+ '<div class="text-xs text-gray-500">{}</div>'
163
+ '<div class="text-xs text-blue-600 dark:text-blue-400">{} subscription{}</div>'
164
+ '</div>',
165
+ obj.user.get_full_name() or obj.user.username,
166
+ obj.user.email,
167
+ total_subscriptions,
168
+ 's' if total_subscriptions != 1 else ''
169
+ )
170
+ return format_html('<span class="text-gray-500">No user</span>')
84
171
 
85
- @display(description="Endpoint Group")
86
- def endpoint_group_display(self, obj):
87
- """Display endpoint group."""
88
- return format_html(
89
- '<strong>{}</strong><br><small>{}</small>',
90
- obj.endpoint_group.display_name,
91
- obj.endpoint_group.description[:40] + '...' if len(obj.endpoint_group.description) > 40 else obj.endpoint_group.description
92
- )
93
-
94
- @display(description="Tier")
172
+ @display(description="Tier", ordering='tier')
95
173
  def tier_display(self, obj):
96
- """Display tier with price."""
174
+ """Display subscription tier with pricing."""
97
175
  tier_colors = {
98
- 'basic': '#28a745',
99
- 'premium': '#ffc107',
100
- 'enterprise': '#dc3545',
176
+ 'free': 'text-gray-600 dark:text-gray-400',
177
+ 'basic': 'text-yellow-600 dark:text-yellow-400',
178
+ 'pro': 'text-blue-600 dark:text-blue-400',
179
+ 'enterprise': 'text-purple-600 dark:text-purple-400'
101
180
  }
102
181
 
103
- color = tier_colors.get(obj.tier, '#6c757d')
182
+ color = tier_colors.get(obj.tier, 'text-gray-600')
104
183
 
105
184
  return format_html(
106
- '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span><br><small>${:.2f}/month</small>',
185
+ '<div>'
186
+ '<div class="font-medium {}">{}</div>'
187
+ '<div class="text-xs text-gray-500">${}/month</div>'
188
+ '</div>',
107
189
  color,
108
190
  obj.get_tier_display(),
109
- obj.monthly_price
191
+ obj.monthly_cost_usd
110
192
  )
111
193
 
112
- @display(description="Status")
194
+ @display(description="Status", ordering='status')
113
195
  def status_display(self, obj):
114
- """Display status with color coding."""
115
- status_colors = {
116
- 'active': '#28a745',
117
- 'inactive': '#6c757d',
118
- 'cancelled': '#dc3545',
119
- 'expired': '#fd7e14',
120
- 'trial': '#17a2b8',
196
+ """Display status with expiry warnings."""
197
+ status_config = {
198
+ Subscription.SubscriptionStatus.ACTIVE: ('', 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', 'Active'),
199
+ Subscription.SubscriptionStatus.EXPIRED: ('', 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', 'Expired'),
200
+ Subscription.SubscriptionStatus.CANCELLED: ('🚫', 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200', 'Cancelled'),
201
+ Subscription.SubscriptionStatus.SUSPENDED: ('⏸️', 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200', 'Suspended'),
121
202
  }
122
203
 
123
- color = status_colors.get(obj.status, '#6c757d')
204
+ icon, color_class, label = status_config.get(
205
+ obj.status,
206
+ ('❓', 'bg-gray-100 text-gray-800', 'Unknown')
207
+ )
124
208
 
125
- return format_html(
126
- '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>',
127
- color,
128
- obj.get_status_display()
209
+ badge = format_html(
210
+ '<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {}">'
211
+ '{} {}'
212
+ '</span>',
213
+ color_class,
214
+ icon,
215
+ label
129
216
  )
217
+
218
+ # Add expiry warning if active and expiring soon
219
+ if obj.status == Subscription.SubscriptionStatus.ACTIVE and obj.expires_at:
220
+ time_until_expiry = obj.expires_at - timezone.now()
221
+ if time_until_expiry < timedelta(days=7):
222
+ warning = format_html(
223
+ '<div class="text-xs text-orange-600 dark:text-orange-400 mt-1">⚠️ Expires soon</div>'
224
+ )
225
+ return format_html('{}<br>{}', badge, warning)
226
+
227
+ return badge
130
228
 
131
229
  @display(description="Usage")
132
230
  def usage_display(self, obj):
133
- """Display usage with progress."""
134
- if obj.usage_limit == 0:
135
- return format_html('<span style="color: #28a745;">Unlimited</span>')
231
+ """Display usage statistics with progress bars."""
232
+ monthly_limit = obj.requests_per_day * 30 # Approximate monthly limit
233
+ monthly_used = obj.total_requests
234
+
235
+ if monthly_limit > 0:
236
+ usage_percentage = (monthly_used / monthly_limit) * 100
237
+
238
+ if usage_percentage >= 90:
239
+ bar_color = "bg-red-500"
240
+ text_color = "text-red-600 dark:text-red-400"
241
+ elif usage_percentage >= 75:
242
+ bar_color = "bg-orange-500"
243
+ text_color = "text-orange-600 dark:text-orange-400"
244
+ else:
245
+ bar_color = "bg-green-500"
246
+ text_color = "text-green-600 dark:text-green-400"
247
+
248
+ return format_html(
249
+ '<div class="w-full">'
250
+ '<div class="flex justify-between text-xs {}">'
251
+ '<span>{:,} / {:,}</span>'
252
+ '<span>{:.1f}%</span>'
253
+ '</div>'
254
+ '<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700 mt-1">'
255
+ '<div class="{} h-2 rounded-full" style="width: {}%"></div>'
256
+ '</div>'
257
+ '</div>',
258
+ text_color,
259
+ monthly_used,
260
+ monthly_limit,
261
+ usage_percentage,
262
+ bar_color,
263
+ min(usage_percentage, 100)
264
+ )
265
+ else:
266
+ # Unlimited plan
267
+ return format_html(
268
+ '<div class="text-center">'
269
+ '<div class="font-bold text-blue-600 dark:text-blue-400">{:,}</div>'
270
+ '<div class="text-xs text-gray-500">Total requests</div>'
271
+ '</div>',
272
+ monthly_used
273
+ )
274
+
275
+ @display(description="Expiry", ordering='expires_at')
276
+ def expiry_display(self, obj):
277
+ """Display expiry information with countdown."""
278
+ if not obj.expires_at:
279
+ return format_html(
280
+ '<div class="text-center text-blue-600 dark:text-blue-400">'
281
+ '<div class="font-bold">∞</div>'
282
+ '<div class="text-xs">Never expires</div>'
283
+ '</div>'
284
+ )
136
285
 
137
- percentage = (obj.usage_current / obj.usage_limit) * 100 if obj.usage_limit > 0 else 0
286
+ now = timezone.now()
138
287
 
139
- if percentage >= 100:
140
- color = '#dc3545'
141
- elif percentage >= 80:
142
- color = '#ffc107'
288
+ if obj.expires_at <= now:
289
+ # Already expired
290
+ return format_html(
291
+ '<div class="text-center text-red-600 dark:text-red-400">'
292
+ '<div class="font-bold">Expired</div>'
293
+ '<div class="text-xs">{}</div>'
294
+ '</div>',
295
+ naturaltime(obj.expires_at)
296
+ )
297
+
298
+ time_remaining = obj.expires_at - now
299
+
300
+ if time_remaining < timedelta(days=1):
301
+ color = "text-red-600 dark:text-red-400"
302
+ icon = "🚨"
303
+ elif time_remaining < timedelta(days=7):
304
+ color = "text-orange-600 dark:text-orange-400"
305
+ icon = "⚠️"
143
306
  else:
144
- color = '#28a745'
307
+ color = "text-green-600 dark:text-green-400"
308
+ icon = "✅"
145
309
 
146
310
  return format_html(
147
- '<span style="color: {};">{}/{}</span><br><small>{:.1f}%</small>',
311
+ '<div class="text-center {}">'
312
+ '<div><span class="mr-1">{}</span>{}</div>'
313
+ '<div class="text-xs">{}</div>'
314
+ '</div>',
148
315
  color,
149
- obj.usage_current,
150
- obj.usage_limit,
151
- percentage
316
+ icon,
317
+ naturaltime(obj.expires_at),
318
+ obj.expires_at.strftime('%Y-%m-%d')
319
+ )
320
+
321
+ @display(description="Created", ordering='created_at')
322
+ def created_at_display(self, obj):
323
+ """Display creation date."""
324
+ return format_html(
325
+ '<div class="text-xs">'
326
+ '<div>{}</div>'
327
+ '<div class="text-gray-500">{}</div>'
328
+ '</div>',
329
+ obj.created_at.strftime('%Y-%m-%d'),
330
+ naturaltime(obj.created_at)
331
+ )
332
+
333
+ def changelist_view(self, request, extra_context=None):
334
+ """Add subscription statistics to changelist context."""
335
+ extra_context = extra_context or {}
336
+
337
+ try:
338
+ # Basic statistics
339
+ total_subscriptions = Subscription.objects.count()
340
+
341
+ # Status distribution
342
+ status_stats = {}
343
+ for status in Subscription.SubscriptionStatus:
344
+ count = Subscription.objects.filter(status=status).count()
345
+ status_stats[status] = count
346
+
347
+ # Tier distribution
348
+ tier_stats = Subscription.objects.values('tier').annotate(
349
+ count=Count('id')
350
+ ).order_by('tier')
351
+
352
+ # Revenue statistics
353
+ revenue_stats = Subscription.objects.filter(
354
+ status=Subscription.SubscriptionStatus.ACTIVE
355
+ ).values('tier').annotate(
356
+ count=Count('id'),
357
+ revenue=Sum('monthly_cost_usd')
358
+ )
359
+
360
+ # Expiry alerts
361
+ now = timezone.now()
362
+ expiring_soon = Subscription.objects.filter(
363
+ status=Subscription.SubscriptionStatus.ACTIVE,
364
+ expires_at__lte=now + timedelta(days=7),
365
+ expires_at__gt=now
366
+ ).count()
367
+
368
+ recently_expired = Subscription.objects.filter(
369
+ status=Subscription.SubscriptionStatus.EXPIRED,
370
+ expires_at__gte=now - timedelta(days=7)
371
+ ).count()
372
+
373
+ # Usage statistics
374
+ high_usage_subscriptions = Subscription.objects.filter(
375
+ status=Subscription.SubscriptionStatus.ACTIVE,
376
+ total_requests__gte=1000
377
+ ).count()
378
+
379
+ extra_context.update({
380
+ 'subscription_stats': {
381
+ 'total_subscriptions': total_subscriptions,
382
+ 'status_stats': status_stats,
383
+ 'tier_stats': tier_stats,
384
+ 'revenue_stats': revenue_stats,
385
+ 'expiring_soon': expiring_soon,
386
+ 'recently_expired': recently_expired,
387
+ 'high_usage_subscriptions': high_usage_subscriptions,
388
+ }
389
+ })
390
+
391
+ except Exception as e:
392
+ logger.warning(f"Failed to generate subscription statistics: {e}")
393
+ extra_context['subscription_stats'] = None
394
+
395
+ return super().changelist_view(request, extra_context)
396
+
397
+ # ===== ADMIN ACTIONS =====
398
+
399
+ @action(
400
+ description="✅ Activate Subscriptions",
401
+ icon="play_arrow",
402
+ variant=ActionVariant.SUCCESS
403
+ )
404
+ def activate_subscriptions(self, request, queryset):
405
+ """Activate selected subscriptions."""
406
+
407
+ activatable = queryset.filter(
408
+ status__in=[
409
+ Subscription.SubscriptionStatus.SUSPENDED,
410
+ Subscription.SubscriptionStatus.CANCELLED
411
+ ]
152
412
  )
413
+
414
+ activated_count = 0
415
+
416
+ for subscription in activatable:
417
+ try:
418
+ subscription.activate()
419
+ activated_count += 1
420
+
421
+ except Exception as e:
422
+ logger.error(f"Failed to activate subscription {subscription.id}: {e}")
423
+
424
+ if activated_count > 0:
425
+ messages.success(
426
+ request,
427
+ f"✅ Activated {activated_count} subscriptions"
428
+ )
429
+
430
+ skipped = queryset.count() - activated_count
431
+ if skipped > 0:
432
+ messages.info(
433
+ request,
434
+ f"ℹ️ Skipped {skipped} subscriptions (already active or expired)"
435
+ )
153
436
 
154
- @display(description="Expires")
155
- def expires_display(self, obj):
156
- """Display expiration date."""
157
- if obj.expires_at:
158
- return naturaltime(obj.expires_at)
159
- return '—'
437
+ @action(
438
+ description="⏸️ Suspend Subscriptions",
439
+ icon="pause",
440
+ variant=ActionVariant.WARNING
441
+ )
442
+ def suspend_subscriptions(self, request, queryset):
443
+ """Suspend selected subscriptions."""
444
+
445
+ suspendable = queryset.filter(
446
+ status=Subscription.SubscriptionStatus.ACTIVE
447
+ )
448
+
449
+ suspended_count = 0
450
+
451
+ for subscription in suspendable:
452
+ try:
453
+ subscription.suspend(reason=f"Suspended by admin {request.user.username}")
454
+ suspended_count += 1
455
+
456
+ except Exception as e:
457
+ logger.error(f"Failed to suspend subscription {subscription.id}: {e}")
458
+
459
+ if suspended_count > 0:
460
+ messages.success(
461
+ request,
462
+ f"⏸️ Suspended {suspended_count} subscriptions"
463
+ )
464
+
465
+ skipped = queryset.count() - suspended_count
466
+ if skipped > 0:
467
+ messages.info(
468
+ request,
469
+ f"ℹ️ Skipped {skipped} subscriptions (not active)"
470
+ )
471
+
472
+ @action(
473
+ description="📅 Extend Subscriptions (30 days)",
474
+ icon="schedule",
475
+ variant=ActionVariant.INFO
476
+ )
477
+ def extend_subscriptions(self, request, queryset):
478
+ """Extend selected subscriptions by 30 days."""
479
+
480
+ extendable = queryset.filter(
481
+ status__in=[
482
+ Subscription.SubscriptionStatus.ACTIVE,
483
+ Subscription.SubscriptionStatus.EXPIRED
484
+ ]
485
+ )
486
+
487
+ extended_count = 0
488
+
489
+ for subscription in extendable:
490
+ try:
491
+ subscription.renew(duration_days=30)
492
+ extended_count += 1
493
+
494
+ except Exception as e:
495
+ logger.error(f"Failed to extend subscription {subscription.id}: {e}")
496
+
497
+ if extended_count > 0:
498
+ messages.success(
499
+ request,
500
+ f"📅 Extended {extended_count} subscriptions by 30 days"
501
+ )
502
+
503
+ skipped = queryset.count() - extended_count
504
+ if skipped > 0:
505
+ messages.info(
506
+ request,
507
+ f"ℹ️ Skipped {skipped} subscriptions (cancelled or suspended)"
508
+ )
160
509
 
161
510
 
162
511
  @admin.register(EndpointGroup)
@@ -165,63 +514,164 @@ class EndpointGroupAdmin(ModelAdmin):
165
514
 
166
515
  list_display = [
167
516
  'name',
168
- 'display_name',
169
- 'pricing_display',
170
- 'limits_display',
171
- 'is_active',
517
+ 'description',
518
+ 'tariff_count_display',
172
519
  'created_at_display'
173
520
  ]
174
521
 
175
- list_display_links = ['name', 'display_name']
522
+ search_fields = ['name', 'description']
176
523
 
177
- search_fields = ['name', 'display_name', 'description']
524
+ readonly_fields = ['created_at', 'updated_at']
178
525
 
179
- list_filter = ['is_active', 'require_api_key', 'created_at']
526
+ @display(description="Tariffs")
527
+ def tariff_count_display(self, obj):
528
+ """Display tariff count."""
529
+ count = obj.tariffendpointgroup_set.count()
530
+ return format_html(
531
+ '<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">'
532
+ '{} tariff{}'
533
+ '</span>',
534
+ count,
535
+ 's' if count != 1 else ''
536
+ )
180
537
 
181
- fieldsets = [
182
- ('Basic Information', {
183
- 'fields': ['name', 'display_name', 'description']
184
- }),
185
- ('Pricing Tiers', {
186
- 'fields': ['basic_price', 'premium_price', 'enterprise_price']
187
- }),
188
- ('Usage Limits', {
189
- 'fields': ['basic_limit', 'premium_limit', 'enterprise_limit']
190
- }),
191
- ('Settings', {
192
- 'fields': ['is_active', 'require_api_key']
193
- })
538
+ @display(description="Created", ordering='created_at')
539
+ def created_at_display(self, obj):
540
+ """Display creation date."""
541
+ return naturaltime(obj.created_at)
542
+
543
+
544
+ @admin.register(Tariff)
545
+ class TariffAdmin(ModelAdmin):
546
+ """Admin interface for tariffs with endpoint group management."""
547
+
548
+ list_display = [
549
+ 'name',
550
+ 'tier_display',
551
+ 'price_display',
552
+ 'endpoint_groups_display',
553
+ 'subscription_count_display',
554
+ 'is_active'
194
555
  ]
195
556
 
196
- @display(description="Pricing")
197
- def pricing_display(self, obj):
198
- """Display pricing tiers."""
557
+ list_filter = ['is_active', 'is_public', 'created_at']
558
+
559
+ search_fields = ['name', 'description']
560
+
561
+ readonly_fields = ['created_at', 'updated_at']
562
+
563
+ inlines = [TariffEndpointGroupInline]
564
+
565
+ @display(description="Tier", ordering='tier')
566
+ def tier_display(self, obj):
567
+ """Display tier with badge."""
568
+ tier_config = {
569
+ 'free': ('🆓', 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'),
570
+ 'basic': ('🥉', 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'),
571
+ 'premium': ('🥈', 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'),
572
+ 'enterprise': ('🥇', 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'),
573
+ }
574
+
575
+ icon, color_class = tier_config.get(obj.tier, ('📋', 'bg-gray-100 text-gray-800'))
576
+
199
577
  return format_html(
200
- '<div style="line-height: 1.4;">'
201
- 'Basic: <strong>${:.2f}</strong><br>'
202
- 'Premium: <strong>${:.2f}</strong><br>'
203
- 'Enterprise: <strong>${:.2f}</strong>'
204
- '</div>',
205
- obj.basic_price,
206
- obj.premium_price,
207
- obj.enterprise_price
578
+ '<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {}">'
579
+ '{} {}'
580
+ '</span>',
581
+ color_class,
582
+ icon,
583
+ obj.tier.title()
208
584
  )
209
585
 
210
- @display(description="Limits")
211
- def limits_display(self, obj):
212
- """Display usage limits."""
586
+ @display(description="Price", ordering='monthly_price_usd')
587
+ def price_display(self, obj):
588
+ """Display price with formatting."""
589
+ if obj.monthly_price_usd == 0:
590
+ return format_html(
591
+ '<span class="font-bold text-green-600 dark:text-green-400">FREE</span>'
592
+ )
593
+ else:
594
+ return format_html(
595
+ '<span class="font-bold text-blue-600 dark:text-blue-400">${}/month</span>',
596
+ obj.monthly_price_usd
597
+ )
598
+
599
+ @display(description="Endpoint Groups")
600
+ def endpoint_groups_display(self, obj):
601
+ """Display endpoint groups count."""
602
+ count = obj.endpoint_groups.count()
213
603
  return format_html(
214
- '<div style="line-height: 1.4;">'
215
- 'Basic: <strong>{:,}</strong><br>'
216
- 'Premium: <strong>{:,}</strong><br>'
217
- 'Enterprise: <strong>{:,}</strong>'
604
+ '<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">'
605
+ '{} group{}'
606
+ '</span>',
607
+ count,
608
+ 's' if count != 1 else ''
609
+ )
610
+
611
+ @display(description="Subscriptions")
612
+ def subscription_count_display(self, obj):
613
+ """Display active subscription count."""
614
+ count = obj.subscription_set.filter(
615
+ status=Subscription.SubscriptionStatus.ACTIVE
616
+ ).count()
617
+
618
+ if count > 0:
619
+ return format_html(
620
+ '<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">'
621
+ '{} active'
622
+ '</span>',
623
+ count
624
+ )
625
+
626
+ return format_html(
627
+ '<span class="text-gray-500">No active</span>'
628
+ )
629
+
630
+
631
+ @admin.register(TariffEndpointGroup)
632
+ class TariffEndpointGroupAdmin(ModelAdmin):
633
+ """Admin interface for tariff endpoint group relationships."""
634
+
635
+ list_display = [
636
+ 'tariff_display',
637
+ 'endpoint_group_display',
638
+ 'custom_rate_limit_display',
639
+ 'is_enabled'
640
+ ]
641
+
642
+ list_filter = ['is_enabled', 'endpoint_group']
643
+
644
+ search_fields = [
645
+ 'tariff__name',
646
+ 'endpoint_group__name'
647
+ ]
648
+
649
+ @display(description="Tariff", ordering='tariff__name')
650
+ def tariff_display(self, obj):
651
+ """Display tariff with tier."""
652
+ return format_html(
653
+ '<div>'
654
+ '<div class="font-medium">{}</div>'
655
+ '<div class="text-xs text-gray-500">${}/month</div>'
218
656
  '</div>',
219
- obj.basic_limit,
220
- obj.premium_limit,
221
- obj.enterprise_limit if obj.enterprise_limit > 0 else '∞'
657
+ obj.tariff.name,
658
+ obj.tariff.monthly_price_usd
222
659
  )
223
660
 
224
- @display(description="Created")
225
- def created_at_display(self, obj):
226
- """Display creation date."""
227
- return naturaltime(obj.created_at)
661
+ @display(description="Endpoint Group", ordering='endpoint_group__name')
662
+ def endpoint_group_display(self, obj):
663
+ """Display endpoint group."""
664
+ return obj.endpoint_group.name
665
+
666
+ @display(description="Custom Rate Limit", ordering='custom_rate_limit')
667
+ def custom_rate_limit_display(self, obj):
668
+ """Display custom rate limit."""
669
+ if obj.custom_rate_limit:
670
+ return format_html(
671
+ '<span class="font-mono text-orange-600 dark:text-orange-400">{:,}/hour</span>',
672
+ obj.custom_rate_limit
673
+ )
674
+ else:
675
+ return format_html(
676
+ '<span class="text-gray-500">Use tariff default</span>'
677
+ )