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,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)