django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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 (125) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
  3. django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
  4. django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
  5. django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
  6. django_cfg/apps/payments/admin/__init__.py +23 -0
  7. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  8. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  9. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  10. django_cfg/apps/payments/admin/filters.py +259 -0
  11. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  12. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  13. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  14. django_cfg/apps/payments/config/__init__.py +65 -0
  15. django_cfg/apps/payments/config/module.py +70 -0
  16. django_cfg/apps/payments/config/providers.py +115 -0
  17. django_cfg/apps/payments/config/settings.py +96 -0
  18. django_cfg/apps/payments/config/utils.py +52 -0
  19. django_cfg/apps/payments/decorators.py +291 -0
  20. django_cfg/apps/payments/management/__init__.py +3 -0
  21. django_cfg/apps/payments/management/commands/README.md +178 -0
  22. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  23. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  24. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  25. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  26. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  27. django_cfg/apps/payments/middleware/api_access.py +294 -0
  28. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  29. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  30. django_cfg/apps/payments/migrations/0001_initial.py +125 -11
  31. django_cfg/apps/payments/models/__init__.py +18 -0
  32. django_cfg/apps/payments/models/api_keys.py +2 -2
  33. django_cfg/apps/payments/models/balance.py +2 -2
  34. django_cfg/apps/payments/models/base.py +16 -0
  35. django_cfg/apps/payments/models/events.py +2 -2
  36. django_cfg/apps/payments/models/payments.py +112 -2
  37. django_cfg/apps/payments/models/subscriptions.py +2 -2
  38. django_cfg/apps/payments/services/__init__.py +64 -7
  39. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  40. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  41. django_cfg/apps/payments/services/cache/base.py +30 -0
  42. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  43. django_cfg/apps/payments/services/core/__init__.py +17 -0
  44. django_cfg/apps/payments/services/core/balance_service.py +447 -0
  45. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  46. django_cfg/apps/payments/services/core/payment_service.py +576 -0
  47. django_cfg/apps/payments/services/core/subscription_service.py +614 -0
  48. django_cfg/apps/payments/services/internal_types.py +297 -0
  49. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  50. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  51. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  52. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  53. django_cfg/apps/payments/services/providers/__init__.py +22 -0
  54. django_cfg/apps/payments/services/providers/base.py +137 -0
  55. django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
  56. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  57. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  58. django_cfg/apps/payments/services/providers/registry.py +103 -0
  59. django_cfg/apps/payments/services/security/__init__.py +34 -0
  60. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  61. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  62. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  63. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  64. django_cfg/apps/payments/signals/__init__.py +13 -0
  65. django_cfg/apps/payments/signals/api_key_signals.py +160 -0
  66. django_cfg/apps/payments/signals/payment_signals.py +128 -0
  67. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  68. django_cfg/apps/payments/tasks/__init__.py +12 -0
  69. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  70. django_cfg/apps/payments/urls.py +5 -5
  71. django_cfg/apps/payments/utils/__init__.py +45 -0
  72. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  73. django_cfg/apps/payments/utils/config_utils.py +245 -0
  74. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  75. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  76. django_cfg/apps/payments/views/payment_views.py +40 -2
  77. django_cfg/apps/payments/views/webhook_views.py +266 -0
  78. django_cfg/apps/payments/viewsets.py +65 -0
  79. django_cfg/apps/support/signals.py +16 -4
  80. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  81. django_cfg/cli/README.md +2 -2
  82. django_cfg/cli/commands/create_project.py +1 -1
  83. django_cfg/cli/commands/info.py +1 -1
  84. django_cfg/cli/main.py +1 -1
  85. django_cfg/cli/utils.py +5 -5
  86. django_cfg/core/config.py +18 -4
  87. django_cfg/models/payments.py +546 -0
  88. django_cfg/models/revolution.py +1 -1
  89. django_cfg/models/tasks.py +51 -2
  90. django_cfg/modules/base.py +12 -6
  91. django_cfg/modules/django_currency/README.md +104 -269
  92. django_cfg/modules/django_currency/__init__.py +99 -41
  93. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  94. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  95. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  96. django_cfg/modules/django_currency/core/__init__.py +42 -0
  97. django_cfg/modules/django_currency/core/converter.py +169 -0
  98. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  99. django_cfg/modules/django_currency/core/models.py +54 -0
  100. django_cfg/modules/django_currency/database/__init__.py +25 -0
  101. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  102. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  103. django_cfg/modules/django_currency/utils/cache.py +92 -0
  104. django_cfg/modules/django_email.py +42 -4
  105. django_cfg/modules/django_unfold/dashboard.py +20 -0
  106. django_cfg/registry/core.py +10 -0
  107. django_cfg/template_archive/__init__.py +0 -0
  108. django_cfg/template_archive/django_sample.zip +0 -0
  109. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
  110. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
  111. django_cfg/apps/agents/examples/__init__.py +0 -3
  112. django_cfg/apps/agents/examples/simple_example.py +0 -161
  113. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  114. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  115. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  116. django_cfg/apps/payments/services/base.py +0 -68
  117. django_cfg/apps/payments/services/nowpayments.py +0 -78
  118. django_cfg/apps/payments/services/providers.py +0 -77
  119. django_cfg/apps/payments/services/redis_service.py +0 -215
  120. django_cfg/modules/django_currency/cache.py +0 -430
  121. django_cfg/modules/django_currency/converter.py +0 -324
  122. django_cfg/modules/django_currency/service.py +0 -277
  123. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  124. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  125. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,259 @@
1
+ """
2
+ Custom admin filters for payments app.
3
+ """
4
+
5
+ from django.contrib import admin
6
+ from django.utils import timezone
7
+ from django.utils.translation import gettext_lazy as _
8
+ from django.db import models
9
+ from datetime import timedelta
10
+
11
+
12
+ class PaymentStatusFilter(admin.SimpleListFilter):
13
+ """Filter payments by status."""
14
+ title = _('Payment Status')
15
+ parameter_name = 'payment_status'
16
+
17
+ def lookups(self, request, model_admin):
18
+ return (
19
+ ('pending', _('Pending')),
20
+ ('processing', _('Processing')),
21
+ ('completed', _('Completed')),
22
+ ('failed', _('Failed')),
23
+ ('cancelled', _('Cancelled')),
24
+ ('refunded', _('Refunded')),
25
+ )
26
+
27
+ def queryset(self, request, queryset):
28
+ if self.value():
29
+ return queryset.filter(status=self.value())
30
+ return queryset
31
+
32
+
33
+ class PaymentAmountFilter(admin.SimpleListFilter):
34
+ """Filter payments by amount ranges."""
35
+ title = _('Payment Amount')
36
+ parameter_name = 'payment_amount'
37
+
38
+ def lookups(self, request, model_admin):
39
+ return (
40
+ ('small', _('< $10')),
41
+ ('medium', _('$10 - $100')),
42
+ ('large', _('$100 - $1000')),
43
+ ('enterprise', _('> $1000')),
44
+ )
45
+
46
+ def queryset(self, request, queryset):
47
+ if self.value() == 'small':
48
+ return queryset.filter(amount_usd__lt=10)
49
+ elif self.value() == 'medium':
50
+ return queryset.filter(amount_usd__gte=10, amount_usd__lt=100)
51
+ elif self.value() == 'large':
52
+ return queryset.filter(amount_usd__gte=100, amount_usd__lt=1000)
53
+ elif self.value() == 'enterprise':
54
+ return queryset.filter(amount_usd__gte=1000)
55
+ return queryset
56
+
57
+
58
+ class SubscriptionStatusFilter(admin.SimpleListFilter):
59
+ """Filter subscriptions by status."""
60
+ title = _('Subscription Status')
61
+ parameter_name = 'subscription_status'
62
+
63
+ def lookups(self, request, model_admin):
64
+ return (
65
+ ('active', _('Active')),
66
+ ('inactive', _('Inactive')),
67
+ ('cancelled', _('Cancelled')),
68
+ ('expired', _('Expired')),
69
+ ('trial', _('Trial')),
70
+ )
71
+
72
+ def queryset(self, request, queryset):
73
+ if self.value():
74
+ return queryset.filter(status=self.value())
75
+ return queryset
76
+
77
+
78
+ class SubscriptionTierFilter(admin.SimpleListFilter):
79
+ """Filter subscriptions by tier."""
80
+ title = _('Subscription Tier')
81
+ parameter_name = 'subscription_tier'
82
+
83
+ def lookups(self, request, model_admin):
84
+ return (
85
+ ('basic', _('Basic')),
86
+ ('premium', _('Premium')),
87
+ ('enterprise', _('Enterprise')),
88
+ )
89
+
90
+ def queryset(self, request, queryset):
91
+ if self.value():
92
+ return queryset.filter(tier=self.value())
93
+ return queryset
94
+
95
+
96
+ class UsageExceededFilter(admin.SimpleListFilter):
97
+ """Filter subscriptions by usage status."""
98
+ title = _('Usage Status')
99
+ parameter_name = 'usage_status'
100
+
101
+ def lookups(self, request, model_admin):
102
+ return (
103
+ ('exceeded', _('Usage Exceeded')),
104
+ ('high', _('High Usage (>80%)')),
105
+ ('normal', _('Normal Usage')),
106
+ ('unused', _('No Usage')),
107
+ )
108
+
109
+ def queryset(self, request, queryset):
110
+ if self.value() == 'exceeded':
111
+ return queryset.filter(usage_current__gte=models.F('usage_limit'))
112
+ elif self.value() == 'high':
113
+ return queryset.filter(
114
+ usage_current__gte=models.F('usage_limit') * 0.8,
115
+ usage_current__lt=models.F('usage_limit')
116
+ )
117
+ elif self.value() == 'normal':
118
+ return queryset.filter(
119
+ usage_current__gt=0,
120
+ usage_current__lt=models.F('usage_limit') * 0.8
121
+ )
122
+ elif self.value() == 'unused':
123
+ return queryset.filter(usage_current=0)
124
+ return queryset
125
+
126
+
127
+ class APIKeyStatusFilter(admin.SimpleListFilter):
128
+ """Filter API keys by status."""
129
+ title = _('API Key Status')
130
+ parameter_name = 'api_key_status'
131
+
132
+ def lookups(self, request, model_admin):
133
+ return (
134
+ ('active', _('Active')),
135
+ ('inactive', _('Inactive')),
136
+ ('expired', _('Expired')),
137
+ ('unused', _('Never Used')),
138
+ ('recent', _('Used Recently')),
139
+ )
140
+
141
+ def queryset(self, request, queryset):
142
+ now = timezone.now()
143
+ if self.value() == 'active':
144
+ return queryset.filter(is_active=True, expires_at__gt=now)
145
+ elif self.value() == 'inactive':
146
+ return queryset.filter(is_active=False)
147
+ elif self.value() == 'expired':
148
+ return queryset.filter(expires_at__lte=now)
149
+ elif self.value() == 'unused':
150
+ return queryset.filter(last_used__isnull=True)
151
+ elif self.value() == 'recent':
152
+ return queryset.filter(last_used__gte=now - timedelta(days=7))
153
+ return queryset
154
+
155
+
156
+ class CurrencyTypeFilter(admin.SimpleListFilter):
157
+ """Filter currencies by type."""
158
+ title = _('Currency Type')
159
+ parameter_name = 'currency_type'
160
+
161
+ def lookups(self, request, model_admin):
162
+ return (
163
+ ('fiat', _('Fiat Currency')),
164
+ ('crypto', _('Cryptocurrency')),
165
+ )
166
+
167
+ def queryset(self, request, queryset):
168
+ if self.value():
169
+ return queryset.filter(currency_type=self.value())
170
+ return queryset
171
+
172
+
173
+ class TransactionTypeFilter(admin.SimpleListFilter):
174
+ """Filter transactions by type."""
175
+ title = _('Transaction Type')
176
+ parameter_name = 'transaction_type'
177
+
178
+ def lookups(self, request, model_admin):
179
+ return (
180
+ ('credit', _('Credit')),
181
+ ('debit', _('Debit')),
182
+ ('refund', _('Refund')),
183
+ ('withdrawal', _('Withdrawal')),
184
+ )
185
+
186
+ def queryset(self, request, queryset):
187
+ if self.value():
188
+ return queryset.filter(transaction_type=self.value())
189
+ return queryset
190
+
191
+
192
+ class RecentActivityFilter(admin.SimpleListFilter):
193
+ """Filter by recent activity."""
194
+ title = _('Recent Activity')
195
+ parameter_name = 'recent_activity'
196
+
197
+ def lookups(self, request, model_admin):
198
+ return (
199
+ ('1hour', _('Last Hour')),
200
+ ('24hours', _('Last 24 Hours')),
201
+ ('7days', _('Last 7 Days')),
202
+ ('30days', _('Last 30 Days')),
203
+ )
204
+
205
+ def queryset(self, request, queryset):
206
+ now = timezone.now()
207
+ if self.value() == '1hour':
208
+ return queryset.filter(created_at__gte=now - timedelta(hours=1))
209
+ elif self.value() == '24hours':
210
+ return queryset.filter(created_at__gte=now - timedelta(hours=24))
211
+ elif self.value() == '7days':
212
+ return queryset.filter(created_at__gte=now - timedelta(days=7))
213
+ elif self.value() == '30days':
214
+ return queryset.filter(created_at__gte=now - timedelta(days=30))
215
+ return queryset
216
+
217
+
218
+ class UserEmailFilter(admin.SimpleListFilter):
219
+ """Filter by user email using text input."""
220
+ title = _('User Email')
221
+ parameter_name = 'user_email'
222
+
223
+ def lookups(self, request, model_admin):
224
+ """Return empty lookups to show text input."""
225
+ return ()
226
+
227
+ def queryset(self, request, queryset):
228
+ """Filter queryset based on user email input."""
229
+ if self.value():
230
+ return queryset.filter(user__email__icontains=self.value())
231
+ return queryset
232
+
233
+
234
+ class BalanceRangeFilter(admin.SimpleListFilter):
235
+ """Filter user balances by amount ranges."""
236
+ title = _('Balance Amount')
237
+ parameter_name = 'balance_amount'
238
+
239
+ def lookups(self, request, model_admin):
240
+ return (
241
+ ('zero', _('$0')),
242
+ ('low', _('$0.01 - $10')),
243
+ ('medium', _('$10 - $100')),
244
+ ('high', _('$100 - $1000')),
245
+ ('enterprise', _('> $1000')),
246
+ )
247
+
248
+ def queryset(self, request, queryset):
249
+ if self.value() == 'zero':
250
+ return queryset.filter(amount_usd=0)
251
+ elif self.value() == 'low':
252
+ return queryset.filter(amount_usd__gt=0, amount_usd__lte=10)
253
+ elif self.value() == 'medium':
254
+ return queryset.filter(amount_usd__gt=10, amount_usd__lte=100)
255
+ elif self.value() == 'high':
256
+ return queryset.filter(amount_usd__gt=100, amount_usd__lte=1000)
257
+ elif self.value() == 'enterprise':
258
+ return queryset.filter(amount_usd__gt=1000)
259
+ return queryset
@@ -0,0 +1,142 @@
1
+ """
2
+ Admin interface for payments.
3
+ """
4
+
5
+ from django.contrib import admin
6
+ from django.utils.html import format_html
7
+ from django.contrib.humanize.templatetags.humanize import naturaltime
8
+ from unfold.admin import ModelAdmin
9
+ from unfold.decorators import display
10
+
11
+ from ..models import UniversalPayment
12
+ from .filters import PaymentStatusFilter, PaymentAmountFilter, UserEmailFilter, RecentActivityFilter
13
+
14
+
15
+ @admin.register(UniversalPayment)
16
+ class UniversalPaymentAdmin(ModelAdmin):
17
+ """Admin interface for universal payments."""
18
+
19
+ list_display = [
20
+ 'payment_display',
21
+ 'user_display',
22
+ 'amount_display',
23
+ 'status_display',
24
+ 'provider_display',
25
+ 'created_at_display'
26
+ ]
27
+
28
+ list_display_links = ['payment_display']
29
+
30
+ search_fields = [
31
+ 'internal_payment_id',
32
+ 'provider_payment_id',
33
+ 'user__email',
34
+ 'user__first_name',
35
+ 'user__last_name'
36
+ ]
37
+
38
+ list_filter = [
39
+ PaymentStatusFilter,
40
+ PaymentAmountFilter,
41
+ UserEmailFilter,
42
+ RecentActivityFilter,
43
+ 'provider',
44
+ 'currency_code',
45
+ 'created_at'
46
+ ]
47
+
48
+ readonly_fields = [
49
+ 'internal_payment_id',
50
+ 'provider_payment_id',
51
+ 'created_at',
52
+ 'updated_at'
53
+ ]
54
+
55
+ fieldsets = [
56
+ ('Payment Information', {
57
+ 'fields': ['user', 'amount_usd', 'currency_code', 'description']
58
+ }),
59
+ ('Payment Details', {
60
+ 'fields': ['internal_payment_id', 'provider_payment_id', 'provider', 'status']
61
+ }),
62
+ ('Provider Data', {
63
+ 'fields': ['provider_data'],
64
+ 'classes': ['collapse']
65
+ }),
66
+ ('Timestamps', {
67
+ 'fields': ['created_at', 'updated_at'],
68
+ 'classes': ['collapse']
69
+ })
70
+ ]
71
+
72
+ @display(description="Payment")
73
+ def payment_display(self, obj):
74
+ """Display payment ID and description."""
75
+ return format_html(
76
+ '<strong>#{}</strong><br><small>{}</small>',
77
+ obj.internal_payment_id[:8],
78
+ obj.description[:40] + '...' if len(obj.description) > 40 else obj.description
79
+ )
80
+
81
+ @display(description="User")
82
+ def user_display(self, obj):
83
+ """Display user information."""
84
+ return format_html(
85
+ '<strong>{}</strong><br><small>{}</small>',
86
+ obj.user.get_full_name() or obj.user.email,
87
+ obj.user.email
88
+ )
89
+
90
+ @display(description="Amount")
91
+ def amount_display(self, obj):
92
+ """Display amount with currency."""
93
+ return format_html(
94
+ '<span style="font-weight: bold; font-size: 14px;">${:.2f}</span><br><small>{}</small>',
95
+ obj.amount_usd,
96
+ obj.currency_code
97
+ )
98
+
99
+ @display(description="Status")
100
+ def status_display(self, obj):
101
+ """Display status with color coding."""
102
+ status_colors = {
103
+ 'pending': '#ffc107',
104
+ 'confirming': '#17a2b8',
105
+ 'confirmed': '#28a745',
106
+ 'completed': '#28a745',
107
+ 'failed': '#dc3545',
108
+ 'expired': '#6c757d',
109
+ 'cancelled': '#6c757d',
110
+ 'refunded': '#fd7e14',
111
+ }
112
+
113
+ color = status_colors.get(obj.status, '#6c757d')
114
+
115
+ return format_html(
116
+ '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>',
117
+ color,
118
+ obj.get_status_display()
119
+ )
120
+
121
+ @display(description="Provider")
122
+ def provider_display(self, obj):
123
+ """Display provider with external ID."""
124
+ provider_colors = {
125
+ 'nowpayments': '#007bff',
126
+ 'stripe': '#6f42c1',
127
+ 'internal': '#28a745',
128
+ }
129
+
130
+ color = provider_colors.get(obj.provider, '#6c757d')
131
+
132
+ return format_html(
133
+ '<span style="color: {}; font-weight: bold;">{}</span><br><small>{}</small>',
134
+ color,
135
+ obj.get_provider_display(),
136
+ obj.provider_payment_id[:16] + '...' if obj.provider_payment_id and len(obj.provider_payment_id) > 16 else obj.provider_payment_id or '—'
137
+ )
138
+
139
+ @display(description="Created")
140
+ def created_at_display(self, obj):
141
+ """Display creation date."""
142
+ return naturaltime(obj.created_at)
@@ -0,0 +1,227 @@
1
+ """
2
+ Admin interface for subscriptions.
3
+ """
4
+
5
+ from django.contrib import admin
6
+ from django.utils.html import format_html
7
+ from django.contrib.humanize.templatetags.humanize import naturaltime
8
+ from unfold.admin import ModelAdmin
9
+ from unfold.decorators import display
10
+
11
+ from ..models import Subscription, EndpointGroup
12
+ from .filters import SubscriptionStatusFilter, SubscriptionTierFilter, UsageExceededFilter, UserEmailFilter
13
+
14
+
15
+ @admin.register(Subscription)
16
+ class SubscriptionAdmin(ModelAdmin):
17
+ """Admin interface for subscriptions."""
18
+
19
+ list_display = [
20
+ 'subscription_display',
21
+ 'user_display',
22
+ 'endpoint_group_display',
23
+ 'tier_display',
24
+ 'status_display',
25
+ 'usage_display',
26
+ 'expires_display'
27
+ ]
28
+
29
+ list_display_links = ['subscription_display']
30
+
31
+ search_fields = [
32
+ 'user__email',
33
+ 'endpoint_group__name',
34
+ 'endpoint_group__display_name'
35
+ ]
36
+
37
+ list_filter = [
38
+ SubscriptionStatusFilter,
39
+ SubscriptionTierFilter,
40
+ UsageExceededFilter,
41
+ UserEmailFilter,
42
+ 'endpoint_group',
43
+ 'created_at'
44
+ ]
45
+
46
+ readonly_fields = [
47
+ 'created_at',
48
+ 'updated_at'
49
+ ]
50
+
51
+ fieldsets = [
52
+ ('Subscription Information', {
53
+ 'fields': ['user', 'endpoint_group', 'tier', 'status']
54
+ }),
55
+ ('Billing', {
56
+ 'fields': ['monthly_price', 'last_billed', 'next_billing']
57
+ }),
58
+ ('Usage', {
59
+ 'fields': ['usage_limit', 'usage_current']
60
+ }),
61
+ ('Dates', {
62
+ 'fields': ['expires_at', 'cancelled_at', 'created_at', 'updated_at'],
63
+ 'classes': ['collapse']
64
+ })
65
+ ]
66
+
67
+ @display(description="Subscription")
68
+ def subscription_display(self, obj):
69
+ """Display subscription info."""
70
+ return format_html(
71
+ '<strong>#{}</strong><br><small>{}</small>',
72
+ str(obj.id)[:8],
73
+ obj.endpoint_group.display_name
74
+ )
75
+
76
+ @display(description="User")
77
+ 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
+ )
84
+
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")
95
+ def tier_display(self, obj):
96
+ """Display tier with price."""
97
+ tier_colors = {
98
+ 'basic': '#28a745',
99
+ 'premium': '#ffc107',
100
+ 'enterprise': '#dc3545',
101
+ }
102
+
103
+ color = tier_colors.get(obj.tier, '#6c757d')
104
+
105
+ return format_html(
106
+ '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span><br><small>${:.2f}/month</small>',
107
+ color,
108
+ obj.get_tier_display(),
109
+ obj.monthly_price
110
+ )
111
+
112
+ @display(description="Status")
113
+ 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',
121
+ }
122
+
123
+ color = status_colors.get(obj.status, '#6c757d')
124
+
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()
129
+ )
130
+
131
+ @display(description="Usage")
132
+ 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>')
136
+
137
+ percentage = (obj.usage_current / obj.usage_limit) * 100 if obj.usage_limit > 0 else 0
138
+
139
+ if percentage >= 100:
140
+ color = '#dc3545'
141
+ elif percentage >= 80:
142
+ color = '#ffc107'
143
+ else:
144
+ color = '#28a745'
145
+
146
+ return format_html(
147
+ '<span style="color: {};">{}/{}</span><br><small>{:.1f}%</small>',
148
+ color,
149
+ obj.usage_current,
150
+ obj.usage_limit,
151
+ percentage
152
+ )
153
+
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 '—'
160
+
161
+
162
+ @admin.register(EndpointGroup)
163
+ class EndpointGroupAdmin(ModelAdmin):
164
+ """Admin interface for endpoint groups."""
165
+
166
+ list_display = [
167
+ 'name',
168
+ 'display_name',
169
+ 'pricing_display',
170
+ 'limits_display',
171
+ 'is_active',
172
+ 'created_at_display'
173
+ ]
174
+
175
+ list_display_links = ['name', 'display_name']
176
+
177
+ search_fields = ['name', 'display_name', 'description']
178
+
179
+ list_filter = ['is_active', 'require_api_key', 'created_at']
180
+
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
+ })
194
+ ]
195
+
196
+ @display(description="Pricing")
197
+ def pricing_display(self, obj):
198
+ """Display pricing tiers."""
199
+ 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
208
+ )
209
+
210
+ @display(description="Limits")
211
+ def limits_display(self, obj):
212
+ """Display usage limits."""
213
+ return format_html(
214
+ '<div style="line-height: 1.4;">'
215
+ 'Basic: <strong>{:,}</strong><br>'
216
+ 'Premium: <strong>{:,}</strong><br>'
217
+ 'Enterprise: <strong>{:,}</strong>'
218
+ '</div>',
219
+ obj.basic_limit,
220
+ obj.premium_limit,
221
+ obj.enterprise_limit if obj.enterprise_limit > 0 else '∞'
222
+ )
223
+
224
+ @display(description="Created")
225
+ def created_at_display(self, obj):
226
+ """Display creation date."""
227
+ return naturaltime(obj.created_at)