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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- django_cfg/apps/payments/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/config/__init__.py +65 -0
- django_cfg/apps/payments/config/module.py +70 -0
- django_cfg/apps/payments/config/providers.py +115 -0
- django_cfg/apps/payments/config/settings.py +96 -0
- django_cfg/apps/payments/config/utils.py +52 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +294 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +125 -11
- django_cfg/apps/payments/models/__init__.py +18 -0
- django_cfg/apps/payments/models/api_keys.py +2 -2
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/base.py +16 -0
- django_cfg/apps/payments/models/events.py +2 -2
- django_cfg/apps/payments/models/payments.py +112 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +64 -7
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +447 -0
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +576 -0
- django_cfg/apps/payments/services/core/subscription_service.py +614 -0
- django_cfg/apps/payments/services/internal_types.py +297 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +22 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +103 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +160 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +45 -0
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +245 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/payments/views/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +12 -6
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- django_cfg/apps/payments/services/base.py +0 -68
- django_cfg/apps/payments/services/nowpayments.py +0 -78
- django_cfg/apps/payments/services/providers.py +0 -77
- django_cfg/apps/payments/services/redis_service.py +0 -215
- django_cfg/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {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)
|