django-cfg 1.2.31__py3-none-any.whl → 1.3.3__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 (264) 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/cleanup_expired_data.py +419 -0
  39. django_cfg/apps/payments/management/commands/currency_stats.py +297 -225
  40. django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
  41. django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
  42. django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
  43. django_cfg/apps/payments/management/commands/test_providers.py +434 -0
  44. django_cfg/apps/payments/middleware/__init__.py +3 -1
  45. django_cfg/apps/payments/middleware/api_access.py +329 -222
  46. django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
  47. django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
  48. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  49. django_cfg/apps/payments/models/__init__.py +13 -18
  50. django_cfg/apps/payments/models/api_keys.py +121 -43
  51. django_cfg/apps/payments/models/balance.py +153 -115
  52. django_cfg/apps/payments/models/base.py +68 -15
  53. django_cfg/apps/payments/models/currencies.py +172 -148
  54. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  55. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  56. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  57. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  58. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  59. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  60. django_cfg/apps/payments/models/payments.py +235 -285
  61. django_cfg/apps/payments/models/subscriptions.py +257 -177
  62. django_cfg/apps/payments/models/tariffs.py +147 -40
  63. django_cfg/apps/payments/services/__init__.py +209 -56
  64. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  65. django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
  66. django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
  67. django_cfg/apps/payments/services/{cache/base.py → cache_service/interfaces.py} +3 -1
  68. django_cfg/apps/payments/services/cache_service/keys.py +49 -0
  69. django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
  70. django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
  71. django_cfg/apps/payments/services/core/__init__.py +10 -6
  72. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  73. django_cfg/apps/payments/services/core/base.py +166 -0
  74. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  75. django_cfg/apps/payments/services/core/payment_service.py +371 -465
  76. django_cfg/apps/payments/services/core/subscription_service.py +425 -481
  77. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  78. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  79. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  80. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  81. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  82. django_cfg/apps/payments/services/providers/base.py +234 -174
  83. django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
  84. django_cfg/apps/payments/services/providers/registry.py +367 -301
  85. django_cfg/apps/payments/services/types/__init__.py +78 -0
  86. django_cfg/apps/payments/services/types/data.py +177 -0
  87. django_cfg/apps/payments/services/types/requests.py +150 -0
  88. django_cfg/apps/payments/services/types/responses.py +156 -0
  89. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  90. django_cfg/apps/payments/signals/__init__.py +33 -8
  91. django_cfg/apps/payments/signals/api_key_signals.py +210 -129
  92. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  93. django_cfg/apps/payments/signals/payment_signals.py +128 -103
  94. django_cfg/apps/payments/signals/subscription_signals.py +194 -142
  95. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  96. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  97. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  98. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  99. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  100. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  101. django_cfg/apps/payments/urls.py +45 -48
  102. django_cfg/apps/payments/urls_admin.py +33 -42
  103. django_cfg/apps/payments/views/api/__init__.py +101 -0
  104. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  105. django_cfg/apps/payments/views/api/balances.py +381 -0
  106. django_cfg/apps/payments/views/api/base.py +298 -0
  107. django_cfg/apps/payments/views/api/currencies.py +402 -0
  108. django_cfg/apps/payments/views/api/payments.py +415 -0
  109. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  110. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  111. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  112. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  113. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  114. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  115. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  116. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  117. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  118. django_cfg/config.py +1 -1
  119. django_cfg/core/config.py +40 -4
  120. django_cfg/core/generation.py +25 -4
  121. django_cfg/core/integration/README.md +363 -0
  122. django_cfg/core/integration/__init__.py +47 -0
  123. django_cfg/core/integration/commands_collector.py +239 -0
  124. django_cfg/core/integration/display/__init__.py +15 -0
  125. django_cfg/core/integration/display/base.py +157 -0
  126. django_cfg/core/integration/display/ngrok.py +164 -0
  127. django_cfg/core/integration/display/startup.py +815 -0
  128. django_cfg/core/integration/url_integration.py +123 -0
  129. django_cfg/core/integration/version_checker.py +160 -0
  130. django_cfg/management/commands/auto_generate.py +4 -0
  131. django_cfg/management/commands/check_settings.py +6 -0
  132. django_cfg/management/commands/clear_constance.py +5 -2
  133. django_cfg/management/commands/create_token.py +6 -0
  134. django_cfg/management/commands/list_urls.py +6 -0
  135. django_cfg/management/commands/migrate_all.py +6 -0
  136. django_cfg/management/commands/migrator.py +3 -0
  137. django_cfg/management/commands/rundramatiq.py +6 -0
  138. django_cfg/management/commands/runserver_ngrok.py +51 -29
  139. django_cfg/management/commands/script.py +6 -0
  140. django_cfg/management/commands/show_config.py +12 -2
  141. django_cfg/management/commands/show_urls.py +4 -0
  142. django_cfg/management/commands/superuser.py +6 -0
  143. django_cfg/management/commands/task_clear.py +4 -1
  144. django_cfg/management/commands/task_status.py +3 -1
  145. django_cfg/management/commands/test_email.py +3 -0
  146. django_cfg/management/commands/test_telegram.py +6 -0
  147. django_cfg/management/commands/test_twilio.py +6 -0
  148. django_cfg/management/commands/tree.py +6 -0
  149. django_cfg/management/commands/validate_config.py +155 -149
  150. django_cfg/models/constance.py +31 -11
  151. django_cfg/models/payments.py +175 -492
  152. django_cfg/modules/django_logger.py +160 -146
  153. django_cfg/modules/django_unfold/dashboard.py +64 -16
  154. django_cfg/registry/core.py +1 -0
  155. django_cfg/template_archive/django_sample.zip +0 -0
  156. django_cfg/utils/smart_defaults.py +227 -570
  157. django_cfg/utils/toolkit.py +51 -11
  158. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/METADATA +4 -1
  159. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/RECORD +162 -185
  160. django_cfg/apps/payments/__init__.py +0 -8
  161. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  162. django_cfg/apps/payments/config/module.py +0 -70
  163. django_cfg/apps/payments/config/providers.py +0 -105
  164. django_cfg/apps/payments/config/settings.py +0 -96
  165. django_cfg/apps/payments/config/utils.py +0 -52
  166. django_cfg/apps/payments/decorators.py +0 -291
  167. django_cfg/apps/payments/management/commands/README.md +0 -146
  168. django_cfg/apps/payments/managers/__init__.py +0 -23
  169. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  170. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  171. django_cfg/apps/payments/managers/currency_manager.py +0 -306
  172. django_cfg/apps/payments/managers/payment_manager.py +0 -192
  173. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  174. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  175. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
  176. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
  177. django_cfg/apps/payments/models/events.py +0 -73
  178. django_cfg/apps/payments/serializers/__init__.py +0 -57
  179. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  180. django_cfg/apps/payments/serializers/balance.py +0 -59
  181. django_cfg/apps/payments/serializers/currencies.py +0 -63
  182. django_cfg/apps/payments/serializers/payments.py +0 -62
  183. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  184. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  185. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  186. django_cfg/apps/payments/services/cache/simple_cache.py +0 -135
  187. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  188. django_cfg/apps/payments/services/internal_types.py +0 -461
  189. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  190. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  191. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
  192. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  193. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
  194. django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
  195. django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
  196. django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
  197. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
  198. django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
  199. django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
  200. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
  201. django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
  202. django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
  203. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
  204. django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
  205. django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
  206. django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
  207. django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
  208. django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
  209. django_cfg/apps/payments/services/security/__init__.py +0 -34
  210. django_cfg/apps/payments/services/security/error_handler.py +0 -635
  211. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  212. django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
  213. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  214. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  215. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  216. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  217. django_cfg/apps/payments/tasks/__init__.py +0 -12
  218. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  219. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
  220. django_cfg/apps/payments/templates/payments/base.html +0 -182
  221. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  222. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  223. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
  224. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  225. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
  226. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
  227. django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
  228. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
  229. django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
  230. django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
  231. django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
  232. django_cfg/apps/payments/templates/payments/stats.html +0 -261
  233. django_cfg/apps/payments/templates/payments/test.html +0 -213
  234. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  235. django_cfg/apps/payments/utils/__init__.py +0 -43
  236. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  237. django_cfg/apps/payments/utils/config_utils.py +0 -239
  238. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  239. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  240. django_cfg/apps/payments/views/__init__.py +0 -63
  241. django_cfg/apps/payments/views/api_key_views.py +0 -164
  242. django_cfg/apps/payments/views/balance_views.py +0 -75
  243. django_cfg/apps/payments/views/currency_views.py +0 -122
  244. django_cfg/apps/payments/views/payment_views.py +0 -149
  245. django_cfg/apps/payments/views/subscription_views.py +0 -135
  246. django_cfg/apps/payments/views/tariff_views.py +0 -131
  247. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  248. django_cfg/apps/payments/views/templates/ajax.py +0 -451
  249. django_cfg/apps/payments/views/templates/base.py +0 -212
  250. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  251. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  252. django_cfg/apps/payments/views/templates/payment_management.py +0 -158
  253. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  254. django_cfg/apps/payments/views/templates/stats.py +0 -244
  255. django_cfg/apps/payments/views/templates/utils.py +0 -181
  256. django_cfg/apps/payments/views/webhook_views.py +0 -266
  257. django_cfg/apps/payments/viewsets.py +0 -66
  258. django_cfg/core/integration.py +0 -160
  259. django_cfg/template_archive/.gitignore +0 -1
  260. django_cfg/template_archive/__init__.py +0 -0
  261. django_cfg/urls.py +0 -33
  262. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/WHEEL +0 -0
  263. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/entry_points.txt +0 -0
  264. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.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
+ )