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,434 @@
|
|
1
|
+
"""
|
2
|
+
Admin interfaces for balance and transaction management.
|
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 django.urls import reverse
|
9
|
+
from django.shortcuts import redirect
|
10
|
+
from unfold.admin import ModelAdmin
|
11
|
+
from unfold.decorators import display, action
|
12
|
+
from unfold.enums import ActionVariant
|
13
|
+
|
14
|
+
from ..models import UserBalance, Transaction
|
15
|
+
from .filters import BalanceRangeFilter, TransactionTypeFilter, UserEmailFilter, RecentActivityFilter
|
16
|
+
|
17
|
+
|
18
|
+
@admin.register(UserBalance)
|
19
|
+
class UserBalanceAdmin(ModelAdmin):
|
20
|
+
"""Admin interface for user balances."""
|
21
|
+
|
22
|
+
list_display = [
|
23
|
+
'user_display',
|
24
|
+
'balance_display',
|
25
|
+
'reserved_display',
|
26
|
+
'available_display',
|
27
|
+
'last_transaction_display',
|
28
|
+
'created_at_display'
|
29
|
+
]
|
30
|
+
|
31
|
+
list_display_links = ['user_display']
|
32
|
+
|
33
|
+
search_fields = ['user__email', 'user__first_name', 'user__last_name']
|
34
|
+
|
35
|
+
list_filter = [
|
36
|
+
BalanceRangeFilter,
|
37
|
+
UserEmailFilter,
|
38
|
+
RecentActivityFilter,
|
39
|
+
'created_at'
|
40
|
+
]
|
41
|
+
|
42
|
+
readonly_fields = [
|
43
|
+
'created_at',
|
44
|
+
'updated_at',
|
45
|
+
'transaction_history',
|
46
|
+
'balance_statistics'
|
47
|
+
]
|
48
|
+
|
49
|
+
fieldsets = [
|
50
|
+
('User Information', {
|
51
|
+
'fields': ['user']
|
52
|
+
}),
|
53
|
+
('Balance Details', {
|
54
|
+
'fields': ['amount_usd', 'reserved_usd']
|
55
|
+
}),
|
56
|
+
('Statistics', {
|
57
|
+
'fields': ['balance_statistics'],
|
58
|
+
'classes': ['collapse']
|
59
|
+
}),
|
60
|
+
('Transaction History', {
|
61
|
+
'fields': ['transaction_history'],
|
62
|
+
'classes': ['collapse']
|
63
|
+
}),
|
64
|
+
('Timestamps', {
|
65
|
+
'fields': ['created_at', 'updated_at'],
|
66
|
+
'classes': ['collapse']
|
67
|
+
})
|
68
|
+
]
|
69
|
+
|
70
|
+
actions_detail = ['add_funds', 'view_transactions']
|
71
|
+
|
72
|
+
@display(description="User")
|
73
|
+
def user_display(self, obj):
|
74
|
+
"""Display user with avatar."""
|
75
|
+
user = obj.user
|
76
|
+
if hasattr(user, 'avatar') and user.avatar:
|
77
|
+
avatar_html = f'<img src="{user.avatar.url}" style="width: 24px; height: 24px; border-radius: 50%; margin-right: 8px;" />'
|
78
|
+
else:
|
79
|
+
initials = f"{user.first_name[:1]}{user.last_name[:1]}" if user.first_name and user.last_name else user.email[:2]
|
80
|
+
avatar_html = f'<div style="width: 24px; height: 24px; border-radius: 50%; background: #6c757d; color: white; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; margin-right: 8px;">{initials.upper()}</div>'
|
81
|
+
|
82
|
+
return format_html(
|
83
|
+
'{}<strong>{}</strong><br><small>{}</small>',
|
84
|
+
avatar_html,
|
85
|
+
user.get_full_name() or user.email,
|
86
|
+
user.email
|
87
|
+
)
|
88
|
+
|
89
|
+
@display(description="Balance")
|
90
|
+
def balance_display(self, obj):
|
91
|
+
"""Display balance with color coding."""
|
92
|
+
amount = obj.amount_usd
|
93
|
+
if amount > 100:
|
94
|
+
color = '#28a745' # Green
|
95
|
+
elif amount > 10:
|
96
|
+
color = '#ffc107' # Yellow
|
97
|
+
elif amount > 0:
|
98
|
+
color = '#fd7e14' # Orange
|
99
|
+
else:
|
100
|
+
color = '#dc3545' # Red
|
101
|
+
|
102
|
+
return format_html(
|
103
|
+
'<span style="color: {}; font-weight: bold;">${:.2f}</span>',
|
104
|
+
color, amount
|
105
|
+
)
|
106
|
+
|
107
|
+
@display(description="Reserved")
|
108
|
+
def reserved_display(self, obj):
|
109
|
+
"""Display reserved amount."""
|
110
|
+
if obj.reserved_usd > 0:
|
111
|
+
return format_html(
|
112
|
+
'<span style="color: #6c757d;">${:.2f}</span>',
|
113
|
+
obj.reserved_usd
|
114
|
+
)
|
115
|
+
return "—"
|
116
|
+
|
117
|
+
@display(description="Available")
|
118
|
+
def available_display(self, obj):
|
119
|
+
"""Display available balance."""
|
120
|
+
available = obj.amount_usd - obj.reserved_usd
|
121
|
+
return format_html(
|
122
|
+
'<span style="font-weight: bold;">${:.2f}</span>',
|
123
|
+
available
|
124
|
+
)
|
125
|
+
|
126
|
+
@display(description="Last Transaction")
|
127
|
+
def last_transaction_display(self, obj):
|
128
|
+
"""Display last transaction."""
|
129
|
+
last_transaction = obj.user.transactions.order_by('-created_at').first()
|
130
|
+
if last_transaction:
|
131
|
+
return format_html(
|
132
|
+
'<span style="color: {};">{} ${:.2f}</span><br><small>{}</small>',
|
133
|
+
'#28a745' if last_transaction.amount_usd > 0 else '#dc3545',
|
134
|
+
'+' if last_transaction.amount_usd > 0 else '',
|
135
|
+
abs(last_transaction.amount_usd),
|
136
|
+
naturaltime(last_transaction.created_at)
|
137
|
+
)
|
138
|
+
return "No transactions"
|
139
|
+
|
140
|
+
@display(description="Created")
|
141
|
+
def created_at_display(self, obj):
|
142
|
+
"""Display creation date."""
|
143
|
+
return naturaltime(obj.created_at)
|
144
|
+
|
145
|
+
def balance_statistics(self, obj):
|
146
|
+
"""Show balance statistics."""
|
147
|
+
transactions = obj.user.transactions.all()
|
148
|
+
total_credited = sum(t.amount_usd for t in transactions if t.amount_usd > 0)
|
149
|
+
total_debited = sum(abs(t.amount_usd) for t in transactions if t.amount_usd < 0)
|
150
|
+
transaction_count = transactions.count()
|
151
|
+
|
152
|
+
return format_html(
|
153
|
+
'<div style="line-height: 1.6;">'
|
154
|
+
'<strong>Statistics:</strong><br>'
|
155
|
+
'• Total Credited: <span style="color: #28a745;">${:.2f}</span><br>'
|
156
|
+
'• Total Debited: <span style="color: #dc3545;">${:.2f}</span><br>'
|
157
|
+
'• Net Balance: <span style="color: {};">${:.2f}</span><br>'
|
158
|
+
'• Total Transactions: {}<br>'
|
159
|
+
'• Available Balance: <strong>${:.2f}</strong>'
|
160
|
+
'</div>',
|
161
|
+
total_credited,
|
162
|
+
total_debited,
|
163
|
+
'#28a745' if (total_credited - total_debited) > 0 else '#dc3545',
|
164
|
+
total_credited - total_debited,
|
165
|
+
transaction_count,
|
166
|
+
obj.amount_usd - obj.reserved_usd
|
167
|
+
)
|
168
|
+
|
169
|
+
balance_statistics.short_description = "Balance Statistics"
|
170
|
+
|
171
|
+
def transaction_history(self, obj):
|
172
|
+
"""Show recent transaction history."""
|
173
|
+
transactions = obj.user.transactions.order_by('-created_at')[:10]
|
174
|
+
|
175
|
+
if not transactions:
|
176
|
+
return "No transactions"
|
177
|
+
|
178
|
+
html = '<div style="line-height: 1.8;">'
|
179
|
+
for transaction in transactions:
|
180
|
+
amount_color = '#28a745' if transaction.amount_usd > 0 else '#dc3545'
|
181
|
+
amount_sign = '+' if transaction.amount_usd > 0 else ''
|
182
|
+
|
183
|
+
html += f'''
|
184
|
+
<div style="border-bottom: 1px solid #eee; padding: 4px 0;">
|
185
|
+
<span style="color: {amount_color}; font-weight: bold;">
|
186
|
+
{amount_sign}${abs(transaction.amount_usd):.2f}
|
187
|
+
</span>
|
188
|
+
<span style="margin-left: 10px; color: #6c757d;">
|
189
|
+
{transaction.get_transaction_type_display()}
|
190
|
+
</span>
|
191
|
+
<br>
|
192
|
+
<small style="color: #999;">
|
193
|
+
{transaction.description[:50]}{'...' if len(transaction.description) > 50 else ''}
|
194
|
+
• {naturaltime(transaction.created_at)}
|
195
|
+
</small>
|
196
|
+
</div>
|
197
|
+
'''
|
198
|
+
|
199
|
+
if obj.user.transactions.count() > 10:
|
200
|
+
html += f'<p><small><em>... and {obj.user.transactions.count() - 10} more transactions</em></small></p>'
|
201
|
+
|
202
|
+
html += '</div>'
|
203
|
+
return format_html(html)
|
204
|
+
|
205
|
+
transaction_history.short_description = "Recent Transactions"
|
206
|
+
|
207
|
+
@action(
|
208
|
+
description="💰 Add Funds",
|
209
|
+
icon="attach_money",
|
210
|
+
variant=ActionVariant.SUCCESS
|
211
|
+
)
|
212
|
+
def add_funds(self, request, object_id):
|
213
|
+
"""Add funds to user balance."""
|
214
|
+
# In real implementation, this would redirect to a custom form
|
215
|
+
balance = self.get_object(request, object_id)
|
216
|
+
if balance:
|
217
|
+
self.message_user(
|
218
|
+
request,
|
219
|
+
f"Add funds form would open for {balance.user.email}",
|
220
|
+
level='info'
|
221
|
+
)
|
222
|
+
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
223
|
+
|
224
|
+
@action(
|
225
|
+
description="📊 View Transactions",
|
226
|
+
icon="receipt_long",
|
227
|
+
variant=ActionVariant.INFO
|
228
|
+
)
|
229
|
+
def view_transactions(self, request, object_id):
|
230
|
+
"""View all transactions for this user."""
|
231
|
+
balance = self.get_object(request, object_id)
|
232
|
+
if balance:
|
233
|
+
url = reverse('admin:django_cfg_payments_transaction_changelist')
|
234
|
+
return redirect(f"{url}?user__id__exact={balance.user.id}")
|
235
|
+
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
236
|
+
|
237
|
+
|
238
|
+
@admin.register(Transaction)
|
239
|
+
class TransactionAdmin(ModelAdmin):
|
240
|
+
"""Admin interface for transactions."""
|
241
|
+
|
242
|
+
list_display = [
|
243
|
+
'transaction_display',
|
244
|
+
'user_display',
|
245
|
+
'amount_display',
|
246
|
+
'type_display',
|
247
|
+
'payment_display',
|
248
|
+
'subscription_display',
|
249
|
+
'created_at_display'
|
250
|
+
]
|
251
|
+
|
252
|
+
list_display_links = ['transaction_display']
|
253
|
+
|
254
|
+
search_fields = [
|
255
|
+
'user__email',
|
256
|
+
'description',
|
257
|
+
'payment__internal_payment_id',
|
258
|
+
'subscription__endpoint_group__name'
|
259
|
+
]
|
260
|
+
|
261
|
+
list_filter = [
|
262
|
+
TransactionTypeFilter,
|
263
|
+
UserEmailFilter,
|
264
|
+
RecentActivityFilter,
|
265
|
+
'payment__status',
|
266
|
+
'subscription__status',
|
267
|
+
'created_at'
|
268
|
+
]
|
269
|
+
|
270
|
+
readonly_fields = [
|
271
|
+
'created_at',
|
272
|
+
'transaction_details',
|
273
|
+
'related_objects'
|
274
|
+
]
|
275
|
+
|
276
|
+
fieldsets = [
|
277
|
+
('Transaction Information', {
|
278
|
+
'fields': ['user', 'transaction_type', 'amount_usd', 'description']
|
279
|
+
}),
|
280
|
+
('Related Objects', {
|
281
|
+
'fields': ['payment', 'subscription'],
|
282
|
+
'classes': ['collapse']
|
283
|
+
}),
|
284
|
+
('Additional Data', {
|
285
|
+
'fields': ['metadata', 'related_objects'],
|
286
|
+
'classes': ['collapse']
|
287
|
+
}),
|
288
|
+
('Transaction Details', {
|
289
|
+
'fields': ['transaction_details'],
|
290
|
+
'classes': ['collapse']
|
291
|
+
}),
|
292
|
+
('Timestamps', {
|
293
|
+
'fields': ['created_at'],
|
294
|
+
'classes': ['collapse']
|
295
|
+
})
|
296
|
+
]
|
297
|
+
|
298
|
+
@display(description="Transaction")
|
299
|
+
def transaction_display(self, obj):
|
300
|
+
"""Display transaction ID and description."""
|
301
|
+
return format_html(
|
302
|
+
'<strong>#{}</strong><br><small>{}</small>',
|
303
|
+
str(obj.id)[:8],
|
304
|
+
obj.description[:40] + '...' if len(obj.description) > 40 else obj.description
|
305
|
+
)
|
306
|
+
|
307
|
+
@display(description="User")
|
308
|
+
def user_display(self, obj):
|
309
|
+
"""Display user information."""
|
310
|
+
return format_html(
|
311
|
+
'<strong>{}</strong><br><small>{}</small>',
|
312
|
+
obj.user.get_full_name() or obj.user.email,
|
313
|
+
obj.user.email
|
314
|
+
)
|
315
|
+
|
316
|
+
@display(description="Amount")
|
317
|
+
def amount_display(self, obj):
|
318
|
+
"""Display amount with color coding."""
|
319
|
+
amount = obj.amount_usd
|
320
|
+
color = '#28a745' if amount > 0 else '#dc3545'
|
321
|
+
sign = '+' if amount > 0 else ''
|
322
|
+
|
323
|
+
return format_html(
|
324
|
+
'<span style="color: {}; font-weight: bold; font-size: 14px;">{}</span>',
|
325
|
+
color,
|
326
|
+
f'{sign}${abs(amount):.2f}'
|
327
|
+
)
|
328
|
+
|
329
|
+
@display(description="Type")
|
330
|
+
def type_display(self, obj):
|
331
|
+
"""Display transaction type with badge."""
|
332
|
+
type_colors = {
|
333
|
+
'credit': '#28a745',
|
334
|
+
'debit': '#dc3545',
|
335
|
+
'refund': '#17a2b8',
|
336
|
+
'withdrawal': '#ffc107',
|
337
|
+
}
|
338
|
+
|
339
|
+
color = type_colors.get(obj.transaction_type, '#6c757d')
|
340
|
+
|
341
|
+
return format_html(
|
342
|
+
'<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>',
|
343
|
+
color,
|
344
|
+
obj.get_transaction_type_display()
|
345
|
+
)
|
346
|
+
|
347
|
+
@display(description="Payment")
|
348
|
+
def payment_display(self, obj):
|
349
|
+
"""Display related payment."""
|
350
|
+
if obj.payment:
|
351
|
+
return format_html(
|
352
|
+
'<a href="{}" style="color: #007bff;">#{}</a><br><small>{}</small>',
|
353
|
+
reverse('admin:django_cfg_payments_universalpayment_change', args=[obj.payment.id]),
|
354
|
+
obj.payment.internal_payment_id[:8],
|
355
|
+
obj.payment.get_status_display()
|
356
|
+
)
|
357
|
+
return "—"
|
358
|
+
|
359
|
+
@display(description="Subscription")
|
360
|
+
def subscription_display(self, obj):
|
361
|
+
"""Display related subscription."""
|
362
|
+
if obj.subscription:
|
363
|
+
return format_html(
|
364
|
+
'<a href="{}" style="color: #007bff;">{}</a><br><small>{}</small>',
|
365
|
+
reverse('admin:django_cfg_payments_subscription_change', args=[obj.subscription.id]),
|
366
|
+
obj.subscription.endpoint_group.display_name,
|
367
|
+
obj.subscription.get_tier_display()
|
368
|
+
)
|
369
|
+
return "—"
|
370
|
+
|
371
|
+
@display(description="Created")
|
372
|
+
def created_at_display(self, obj):
|
373
|
+
"""Display creation date."""
|
374
|
+
return naturaltime(obj.created_at)
|
375
|
+
|
376
|
+
def transaction_details(self, obj):
|
377
|
+
"""Show detailed transaction information."""
|
378
|
+
return format_html(
|
379
|
+
'<div style="line-height: 1.6;">'
|
380
|
+
'<strong>Transaction Details:</strong><br>'
|
381
|
+
'• ID: {}<br>'
|
382
|
+
'• User: {} ({})<br>'
|
383
|
+
'• Type: {}<br>'
|
384
|
+
'• Amount: <span style="color: {};">${:.2f}</span><br>'
|
385
|
+
'• Description: {}<br>'
|
386
|
+
'• Created: {}<br>'
|
387
|
+
'{}'
|
388
|
+
'{}'
|
389
|
+
'</div>',
|
390
|
+
obj.id,
|
391
|
+
obj.user.get_full_name() or 'No name',
|
392
|
+
obj.user.email,
|
393
|
+
obj.get_transaction_type_display(),
|
394
|
+
'#28a745' if obj.amount_usd > 0 else '#dc3545',
|
395
|
+
obj.amount_usd,
|
396
|
+
obj.description,
|
397
|
+
obj.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
398
|
+
f'• Payment: {obj.payment.internal_payment_id}<br>' if obj.payment else '',
|
399
|
+
f'• Subscription: {obj.subscription.endpoint_group.name}<br>' if obj.subscription else ''
|
400
|
+
)
|
401
|
+
|
402
|
+
transaction_details.short_description = "Transaction Details"
|
403
|
+
|
404
|
+
def related_objects(self, obj):
|
405
|
+
"""Show related objects."""
|
406
|
+
html = '<div style="line-height: 1.6;">'
|
407
|
+
|
408
|
+
if obj.payment:
|
409
|
+
html += f'''
|
410
|
+
<strong>Related Payment:</strong><br>
|
411
|
+
• ID: {obj.payment.internal_payment_id}<br>
|
412
|
+
• Status: {obj.payment.get_status_display()}<br>
|
413
|
+
• Amount: ${obj.payment.amount_usd:.2f}<br>
|
414
|
+
• Provider: {obj.payment.provider}<br>
|
415
|
+
'''
|
416
|
+
|
417
|
+
if obj.subscription:
|
418
|
+
html += f'''
|
419
|
+
<strong>Related Subscription:</strong><br>
|
420
|
+
• Endpoint: {obj.subscription.endpoint_group.display_name}<br>
|
421
|
+
• Tier: {obj.subscription.get_tier_display()}<br>
|
422
|
+
• Status: {obj.subscription.get_status_display()}<br>
|
423
|
+
• Usage: {obj.subscription.usage_current}/{obj.subscription.usage_limit}<br>
|
424
|
+
'''
|
425
|
+
|
426
|
+
if obj.metadata:
|
427
|
+
html += '<strong>Metadata:</strong><br>'
|
428
|
+
for key, value in obj.metadata.items():
|
429
|
+
html += f'• {key}: {value}<br>'
|
430
|
+
|
431
|
+
html += '</div>'
|
432
|
+
return format_html(html)
|
433
|
+
|
434
|
+
related_objects.short_description = "Related Objects"
|
@@ -0,0 +1,186 @@
|
|
1
|
+
"""
|
2
|
+
Admin interface for currencies.
|
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 Currency, CurrencyNetwork
|
12
|
+
from .filters import CurrencyTypeFilter
|
13
|
+
|
14
|
+
|
15
|
+
@admin.register(Currency)
|
16
|
+
class CurrencyAdmin(ModelAdmin):
|
17
|
+
"""Admin interface for currencies."""
|
18
|
+
|
19
|
+
list_display = [
|
20
|
+
'currency_display',
|
21
|
+
'type_display',
|
22
|
+
'rate_display',
|
23
|
+
'status_display',
|
24
|
+
'created_at_display'
|
25
|
+
]
|
26
|
+
|
27
|
+
list_display_links = ['currency_display']
|
28
|
+
|
29
|
+
search_fields = ['code', 'name', 'symbol']
|
30
|
+
|
31
|
+
list_filter = [
|
32
|
+
CurrencyTypeFilter,
|
33
|
+
'is_active',
|
34
|
+
'created_at'
|
35
|
+
]
|
36
|
+
|
37
|
+
readonly_fields = ['rate_updated_at', 'created_at', 'updated_at']
|
38
|
+
|
39
|
+
fieldsets = [
|
40
|
+
('Currency Information', {
|
41
|
+
'fields': ['code', 'name', 'symbol', 'currency_type']
|
42
|
+
}),
|
43
|
+
('Configuration', {
|
44
|
+
'fields': ['decimal_places', 'min_payment_amount', 'is_active']
|
45
|
+
}),
|
46
|
+
('Exchange Rate', {
|
47
|
+
'fields': ['usd_rate', 'rate_updated_at']
|
48
|
+
}),
|
49
|
+
('Timestamps', {
|
50
|
+
'fields': ['created_at', 'updated_at'],
|
51
|
+
'classes': ['collapse']
|
52
|
+
})
|
53
|
+
]
|
54
|
+
|
55
|
+
@display(description="Currency")
|
56
|
+
def currency_display(self, obj):
|
57
|
+
"""Display currency with symbol."""
|
58
|
+
return format_html(
|
59
|
+
'<strong>{}</strong> {}<br><small>{}</small>',
|
60
|
+
obj.code,
|
61
|
+
obj.symbol,
|
62
|
+
obj.name
|
63
|
+
)
|
64
|
+
|
65
|
+
@display(description="Type")
|
66
|
+
def type_display(self, obj):
|
67
|
+
"""Display currency type with badge."""
|
68
|
+
type_colors = {
|
69
|
+
'fiat': '#28a745',
|
70
|
+
'crypto': '#fd7e14',
|
71
|
+
}
|
72
|
+
|
73
|
+
color = type_colors.get(obj.currency_type, '#6c757d')
|
74
|
+
|
75
|
+
return format_html(
|
76
|
+
'<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>',
|
77
|
+
color,
|
78
|
+
obj.get_currency_type_display()
|
79
|
+
)
|
80
|
+
|
81
|
+
@display(description="USD Rate")
|
82
|
+
def rate_display(self, obj):
|
83
|
+
"""Display exchange rate."""
|
84
|
+
if obj.usd_rate != 1.0:
|
85
|
+
return format_html(
|
86
|
+
'<strong>1 {} = ${:.6f}</strong><br><small>Updated: {}</small>',
|
87
|
+
obj.code,
|
88
|
+
obj.usd_rate,
|
89
|
+
naturaltime(obj.rate_updated_at) if obj.rate_updated_at else 'Never'
|
90
|
+
)
|
91
|
+
return format_html('<span style="color: #6c757d;">Base currency</span>')
|
92
|
+
|
93
|
+
@display(description="Status")
|
94
|
+
def status_display(self, obj):
|
95
|
+
"""Display status badge."""
|
96
|
+
if obj.is_active:
|
97
|
+
return format_html(
|
98
|
+
'<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Active</span>'
|
99
|
+
)
|
100
|
+
else:
|
101
|
+
return format_html(
|
102
|
+
'<span style="background: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Inactive</span>'
|
103
|
+
)
|
104
|
+
|
105
|
+
@display(description="Created")
|
106
|
+
def created_at_display(self, obj):
|
107
|
+
"""Display creation date."""
|
108
|
+
return naturaltime(obj.created_at)
|
109
|
+
|
110
|
+
|
111
|
+
@admin.register(CurrencyNetwork)
|
112
|
+
class CurrencyNetworkAdmin(ModelAdmin):
|
113
|
+
"""Admin interface for currency networks."""
|
114
|
+
|
115
|
+
list_display = [
|
116
|
+
'network_display',
|
117
|
+
'currency_display',
|
118
|
+
'status_display',
|
119
|
+
'confirmations_display',
|
120
|
+
'created_at_display'
|
121
|
+
]
|
122
|
+
|
123
|
+
list_display_links = ['network_display']
|
124
|
+
|
125
|
+
search_fields = ['network_name', 'network_code', 'currency__code', 'currency__name']
|
126
|
+
|
127
|
+
list_filter = ['currency', 'is_active', 'created_at']
|
128
|
+
|
129
|
+
readonly_fields = ['created_at', 'updated_at']
|
130
|
+
|
131
|
+
fieldsets = [
|
132
|
+
('Network Information', {
|
133
|
+
'fields': ['currency', 'network_name', 'network_code']
|
134
|
+
}),
|
135
|
+
('Configuration', {
|
136
|
+
'fields': ['confirmation_blocks', 'is_active']
|
137
|
+
}),
|
138
|
+
('Timestamps', {
|
139
|
+
'fields': ['created_at', 'updated_at'],
|
140
|
+
'classes': ['collapse']
|
141
|
+
})
|
142
|
+
]
|
143
|
+
|
144
|
+
@display(description="Network")
|
145
|
+
def network_display(self, obj):
|
146
|
+
"""Display network information."""
|
147
|
+
return format_html(
|
148
|
+
'<strong>{}</strong><br><small>{}</small>',
|
149
|
+
obj.network_name,
|
150
|
+
obj.network_code
|
151
|
+
)
|
152
|
+
|
153
|
+
@display(description="Currency")
|
154
|
+
def currency_display(self, obj):
|
155
|
+
"""Display currency information."""
|
156
|
+
return format_html(
|
157
|
+
'<strong>{}</strong> {}<br><small>{}</small>',
|
158
|
+
obj.currency.code,
|
159
|
+
obj.currency.symbol,
|
160
|
+
obj.currency.name
|
161
|
+
)
|
162
|
+
|
163
|
+
@display(description="Status")
|
164
|
+
def status_display(self, obj):
|
165
|
+
"""Display status badge."""
|
166
|
+
if obj.is_active:
|
167
|
+
return format_html(
|
168
|
+
'<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Active</span>'
|
169
|
+
)
|
170
|
+
else:
|
171
|
+
return format_html(
|
172
|
+
'<span style="background: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">Inactive</span>'
|
173
|
+
)
|
174
|
+
|
175
|
+
@display(description="Confirmations")
|
176
|
+
def confirmations_display(self, obj):
|
177
|
+
"""Display confirmation blocks."""
|
178
|
+
return format_html(
|
179
|
+
'<span style="font-weight: bold;">{}</span> blocks',
|
180
|
+
obj.confirmation_blocks
|
181
|
+
)
|
182
|
+
|
183
|
+
@display(description="Created")
|
184
|
+
def created_at_display(self, obj):
|
185
|
+
"""Display creation date."""
|
186
|
+
return naturaltime(obj.created_at)
|