django-cfg 1.2.29__py3-none-any.whl → 1.3.1__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/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -9
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +600 -108
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +470 -64
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
- django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +16 -20
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +207 -67
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -284
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +344 -468
- django_cfg/apps/payments/services/core/subscription_service.py +425 -484
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +232 -71
- django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
- django_cfg/apps/payments/services/providers/registry.py +429 -80
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +211 -130
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +129 -98
- django_cfg/apps/payments/signals/subscription_signals.py +195 -143
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +46 -47
- django_cfg/apps/payments/urls_admin.py +49 -0
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +75 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -498
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +65 -12
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -178
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/managers/__init__.py +0 -22
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -83
- django_cfg/apps/payments/managers/payment_manager.py +0 -44
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -56
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -55
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -297
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -637
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/urls_templates.py +0 -52
- django_cfg/apps/payments/utils/__init__.py +0 -45
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -245
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -62
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -111
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -312
- django_cfg/apps/payments/views/templates/base.py +0 -204
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -164
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -240
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -65
- django_cfg/core/integration.py +0 -160
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,40 +1,68 @@
|
|
1
1
|
"""
|
2
|
-
Admin interfaces
|
2
|
+
Balance Admin interfaces with Unfold integration.
|
3
|
+
|
4
|
+
Advanced balance and transaction management with bulk operations.
|
3
5
|
"""
|
4
6
|
|
5
7
|
from django.contrib import admin
|
6
8
|
from django.utils.html import format_html
|
7
|
-
from django.contrib.humanize.templatetags.humanize import naturaltime
|
8
|
-
from django.
|
9
|
+
from django.contrib.humanize.templatetags.humanize import naturaltime, intcomma
|
10
|
+
from django.contrib import messages
|
9
11
|
from django.shortcuts import redirect
|
12
|
+
from django.utils.safestring import mark_safe
|
13
|
+
from django.db.models import Count, Sum, Q, Avg
|
14
|
+
from django.utils import timezone
|
15
|
+
from datetime import timedelta
|
16
|
+
from decimal import Decimal
|
17
|
+
from typing import Optional
|
18
|
+
|
10
19
|
from unfold.admin import ModelAdmin
|
11
20
|
from unfold.decorators import display, action
|
12
21
|
from unfold.enums import ActionVariant
|
13
22
|
|
14
23
|
from ..models import UserBalance, Transaction
|
15
|
-
from .filters import BalanceRangeFilter,
|
24
|
+
from .filters import BalanceRangeFilter, RecentActivityFilter
|
25
|
+
from django_cfg.modules.django_logger import get_logger
|
26
|
+
|
27
|
+
logger = get_logger("balance_admin")
|
16
28
|
|
17
29
|
|
18
30
|
@admin.register(UserBalance)
|
19
31
|
class UserBalanceAdmin(ModelAdmin):
|
20
|
-
"""
|
32
|
+
"""
|
33
|
+
Advanced UserBalance admin with bulk operations and financial monitoring.
|
34
|
+
|
35
|
+
Features:
|
36
|
+
- Balance range filtering and visualization
|
37
|
+
- Bulk balance adjustments with audit trail
|
38
|
+
- Financial statistics and alerts
|
39
|
+
- Transaction history integration
|
40
|
+
- Security features for balance modifications
|
41
|
+
"""
|
42
|
+
|
43
|
+
# Custom template for balance statistics
|
44
|
+
change_list_template = 'admin/payments/balance/change_list.html'
|
21
45
|
|
22
46
|
list_display = [
|
23
47
|
'user_display',
|
24
48
|
'balance_display',
|
25
|
-
'
|
26
|
-
'
|
27
|
-
'
|
49
|
+
'balance_status',
|
50
|
+
'transaction_count_display',
|
51
|
+
'last_activity_display',
|
28
52
|
'created_at_display'
|
29
53
|
]
|
30
54
|
|
31
55
|
list_display_links = ['user_display']
|
32
56
|
|
33
|
-
search_fields = [
|
57
|
+
search_fields = [
|
58
|
+
'user__email',
|
59
|
+
'user__first_name',
|
60
|
+
'user__last_name',
|
61
|
+
'user__username'
|
62
|
+
]
|
34
63
|
|
35
64
|
list_filter = [
|
36
65
|
BalanceRangeFilter,
|
37
|
-
UserEmailFilter,
|
38
66
|
RecentActivityFilter,
|
39
67
|
'created_at'
|
40
68
|
]
|
@@ -42,8 +70,16 @@ class UserBalanceAdmin(ModelAdmin):
|
|
42
70
|
readonly_fields = [
|
43
71
|
'created_at',
|
44
72
|
'updated_at',
|
45
|
-
'
|
46
|
-
|
73
|
+
'last_transaction_at'
|
74
|
+
]
|
75
|
+
|
76
|
+
# Unfold actions
|
77
|
+
actions_list = [
|
78
|
+
'add_funds_bulk',
|
79
|
+
'subtract_funds_bulk',
|
80
|
+
'reset_zero_balances',
|
81
|
+
'export_balance_report',
|
82
|
+
'send_low_balance_alerts'
|
47
83
|
]
|
48
84
|
|
49
85
|
fieldsets = [
|
@@ -51,15 +87,15 @@ class UserBalanceAdmin(ModelAdmin):
|
|
51
87
|
'fields': ['user']
|
52
88
|
}),
|
53
89
|
('Balance Details', {
|
54
|
-
'fields': [
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
'classes': ['collapse']
|
90
|
+
'fields': [
|
91
|
+
'balance_usd',
|
92
|
+
'reserved_usd'
|
93
|
+
]
|
59
94
|
}),
|
60
|
-
('
|
61
|
-
'fields': [
|
62
|
-
|
95
|
+
('Activity Tracking', {
|
96
|
+
'fields': [
|
97
|
+
'last_transaction_at'
|
98
|
+
]
|
63
99
|
}),
|
64
100
|
('Timestamps', {
|
65
101
|
'fields': ['created_at', 'updated_at'],
|
@@ -67,368 +103,627 @@ class UserBalanceAdmin(ModelAdmin):
|
|
67
103
|
})
|
68
104
|
]
|
69
105
|
|
70
|
-
|
106
|
+
def get_queryset(self, request):
|
107
|
+
"""Optimize queryset with user data and transaction counts."""
|
108
|
+
return super().get_queryset(request).select_related('user').annotate(
|
109
|
+
transaction_count=Count('user__transaction_set')
|
110
|
+
)
|
71
111
|
|
72
|
-
@display(description="User")
|
112
|
+
@display(description="User", ordering='user__email')
|
73
113
|
def user_display(self, obj):
|
74
|
-
"""Display user with avatar."""
|
75
|
-
|
76
|
-
|
77
|
-
|
114
|
+
"""Display user information with avatar and details."""
|
115
|
+
if obj.user:
|
116
|
+
display_name = obj.user.get_full_name() or obj.user.username
|
117
|
+
|
118
|
+
# Determine user tier based on balance
|
119
|
+
if obj.balance_usd >= 1000:
|
120
|
+
tier_icon = "🐋"
|
121
|
+
tier_color = "text-purple-600"
|
122
|
+
tier_name = "Whale"
|
123
|
+
elif obj.balance_usd >= 100:
|
124
|
+
tier_icon = "💎"
|
125
|
+
tier_color = "text-blue-600"
|
126
|
+
tier_name = "Premium"
|
127
|
+
elif obj.balance_usd >= 10:
|
128
|
+
tier_icon = "💰"
|
129
|
+
tier_color = "text-green-600"
|
130
|
+
tier_name = "Active"
|
131
|
+
elif obj.balance_usd > 0:
|
132
|
+
tier_icon = "🪙"
|
133
|
+
tier_color = "text-yellow-600"
|
134
|
+
tier_name = "Basic"
|
135
|
+
else:
|
136
|
+
tier_icon = "💸"
|
137
|
+
tier_color = "text-gray-600"
|
138
|
+
tier_name = "Empty"
|
139
|
+
|
140
|
+
return format_html(
|
141
|
+
'<div class="flex items-center space-x-3">'
|
142
|
+
'<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-sm font-bold">'
|
143
|
+
'{}'
|
144
|
+
'</div>'
|
145
|
+
'<div>'
|
146
|
+
'<div class="font-medium text-gray-900 dark:text-gray-100">{}</div>'
|
147
|
+
'<div class="text-xs text-gray-500">{}</div>'
|
148
|
+
'<div class="text-xs {}"><span class="mr-1">{}</span>{}</div>'
|
149
|
+
'</div>'
|
150
|
+
'</div>',
|
151
|
+
display_name[0].upper() if display_name else 'U',
|
152
|
+
display_name,
|
153
|
+
obj.user.email,
|
154
|
+
tier_color,
|
155
|
+
tier_icon,
|
156
|
+
tier_name
|
157
|
+
)
|
158
|
+
return format_html('<span class="text-gray-500">No user</span>')
|
159
|
+
|
160
|
+
@display(description="Balance", ordering='balance_usd')
|
161
|
+
def balance_display(self, obj):
|
162
|
+
"""Display balance with visual indicators and reserved amounts."""
|
163
|
+
balance = obj.balance_usd
|
164
|
+
reserved = obj.reserved_usd or 0
|
165
|
+
available = balance - reserved
|
166
|
+
|
167
|
+
# Color coding based on balance
|
168
|
+
if balance < 0:
|
169
|
+
balance_color = "text-red-600 dark:text-red-400"
|
170
|
+
balance_icon = "⚠️"
|
171
|
+
elif balance == 0:
|
172
|
+
balance_color = "text-gray-600 dark:text-gray-400"
|
173
|
+
balance_icon = "💸"
|
174
|
+
elif balance < 10:
|
175
|
+
balance_color = "text-yellow-600 dark:text-yellow-400"
|
176
|
+
balance_icon = "🪙"
|
177
|
+
elif balance < 100:
|
178
|
+
balance_color = "text-green-600 dark:text-green-400"
|
179
|
+
balance_icon = "💰"
|
78
180
|
else:
|
79
|
-
|
80
|
-
|
181
|
+
balance_color = "text-blue-600 dark:text-blue-400"
|
182
|
+
balance_icon = "💎"
|
81
183
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
184
|
+
html = f'''
|
185
|
+
<div class="text-right">
|
186
|
+
<div class="font-bold text-lg {balance_color}">
|
187
|
+
<span class="mr-1">{balance_icon}</span>${balance:,.2f}
|
188
|
+
</div>
|
189
|
+
'''
|
190
|
+
|
191
|
+
if reserved > 0:
|
192
|
+
html += f'''
|
193
|
+
<div class="text-xs text-orange-600 dark:text-orange-400">
|
194
|
+
Reserved: ${reserved:,.2f}
|
195
|
+
</div>
|
196
|
+
<div class="text-xs text-gray-500">
|
197
|
+
Available: ${available:,.2f}
|
198
|
+
</div>
|
199
|
+
'''
|
200
|
+
|
201
|
+
html += '</div>'
|
202
|
+
|
203
|
+
return format_html(html)
|
88
204
|
|
89
|
-
@display(description="
|
90
|
-
def
|
91
|
-
"""Display balance with
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
205
|
+
@display(description="Status")
|
206
|
+
def balance_status(self, obj):
|
207
|
+
"""Display balance status with alerts."""
|
208
|
+
balance = obj.balance_usd
|
209
|
+
reserved = obj.reserved_usd or 0
|
210
|
+
|
211
|
+
badges = []
|
212
|
+
|
213
|
+
if balance < 0:
|
214
|
+
badges.append('<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900 dark:text-red-200">⚠️ Negative</span>')
|
215
|
+
elif balance == 0:
|
216
|
+
badges.append('<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-900 dark:text-gray-200">💸 Empty</span>')
|
217
|
+
elif balance < 1:
|
218
|
+
badges.append('<span class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">⚠️ Low</span>')
|
99
219
|
else:
|
100
|
-
|
220
|
+
badges.append('<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200">✅ Active</span>')
|
101
221
|
|
102
|
-
|
103
|
-
'<span
|
104
|
-
|
105
|
-
)
|
222
|
+
if reserved > 0:
|
223
|
+
badges.append('<span class="inline-flex items-center rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-800 dark:bg-orange-900 dark:text-orange-200">🔒 Reserved</span>')
|
224
|
+
|
225
|
+
return format_html('<div class="space-y-1">{}</div>', ''.join(badges))
|
106
226
|
|
107
|
-
@display(description="
|
108
|
-
def
|
109
|
-
"""Display
|
110
|
-
|
227
|
+
@display(description="Transactions")
|
228
|
+
def transaction_count_display(self, obj):
|
229
|
+
"""Display transaction count and recent activity."""
|
230
|
+
count = getattr(obj, 'transaction_count', 0)
|
231
|
+
|
232
|
+
if count > 0:
|
233
|
+
# Get recent transaction count (last 7 days)
|
234
|
+
recent_threshold = timezone.now() - timedelta(days=7)
|
235
|
+
recent_count = Transaction.objects.filter(
|
236
|
+
user=obj.user,
|
237
|
+
created_at__gte=recent_threshold
|
238
|
+
).count()
|
239
|
+
|
111
240
|
return format_html(
|
112
|
-
'<
|
113
|
-
|
241
|
+
'<div class="text-center">'
|
242
|
+
'<div class="font-bold text-blue-600 dark:text-blue-400">{}</div>'
|
243
|
+
'<div class="text-xs text-gray-500">total</div>'
|
244
|
+
'{}'
|
245
|
+
'</div>',
|
246
|
+
count,
|
247
|
+
f'<div class="text-xs text-green-600 dark:text-green-400">{recent_count} recent</div>' if recent_count > 0 else ''
|
114
248
|
)
|
115
|
-
|
116
|
-
|
117
|
-
@display(description="Available")
|
118
|
-
def available_display(self, obj):
|
119
|
-
"""Display available balance."""
|
120
|
-
available = obj.amount_usd - obj.reserved_usd
|
249
|
+
|
121
250
|
return format_html(
|
122
|
-
'<
|
123
|
-
|
251
|
+
'<div class="text-center text-gray-500">'
|
252
|
+
'<div>0</div>'
|
253
|
+
'<div class="text-xs">No transactions</div>'
|
254
|
+
'</div>'
|
124
255
|
)
|
125
256
|
|
126
|
-
@display(description="Last
|
127
|
-
def
|
128
|
-
"""Display last transaction."""
|
129
|
-
|
130
|
-
|
257
|
+
@display(description="Last Activity", ordering='last_transaction_at')
|
258
|
+
def last_activity_display(self, obj):
|
259
|
+
"""Display last transaction activity."""
|
260
|
+
if obj.last_transaction_at:
|
261
|
+
time_ago = timezone.now() - obj.last_transaction_at
|
262
|
+
|
263
|
+
if time_ago < timedelta(hours=1):
|
264
|
+
color = "text-green-600 dark:text-green-400"
|
265
|
+
icon = "🟢"
|
266
|
+
elif time_ago < timedelta(days=1):
|
267
|
+
color = "text-yellow-600 dark:text-yellow-400"
|
268
|
+
icon = "🟡"
|
269
|
+
elif time_ago < timedelta(days=7):
|
270
|
+
color = "text-orange-600 dark:text-orange-400"
|
271
|
+
icon = "🟠"
|
272
|
+
else:
|
273
|
+
color = "text-red-600 dark:text-red-400"
|
274
|
+
icon = "🔴"
|
275
|
+
|
131
276
|
return format_html(
|
132
|
-
'<
|
133
|
-
'
|
134
|
-
'
|
135
|
-
|
136
|
-
|
277
|
+
'<div class="text-xs {}">'
|
278
|
+
'<span class="mr-1">{}</span>{}'
|
279
|
+
'</div>',
|
280
|
+
color,
|
281
|
+
icon,
|
282
|
+
naturaltime(obj.last_transaction_at)
|
137
283
|
)
|
138
|
-
|
284
|
+
|
285
|
+
return format_html(
|
286
|
+
'<div class="text-xs text-gray-500">Never</div>'
|
287
|
+
)
|
139
288
|
|
140
|
-
@display(description="Created")
|
289
|
+
@display(description="Created", ordering='created_at')
|
141
290
|
def created_at_display(self, obj):
|
142
291
|
"""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
292
|
return format_html(
|
153
|
-
'<div
|
154
|
-
'<
|
155
|
-
'
|
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>'
|
293
|
+
'<div class="text-xs">'
|
294
|
+
'<div>{}</div>'
|
295
|
+
'<div class="text-gray-500">{}</div>'
|
160
296
|
'</div>',
|
161
|
-
|
162
|
-
|
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
|
297
|
+
obj.created_at.strftime('%Y-%m-%d'),
|
298
|
+
naturaltime(obj.created_at)
|
167
299
|
)
|
168
300
|
|
169
|
-
|
301
|
+
def changelist_view(self, request, extra_context=None):
|
302
|
+
"""Add balance statistics to changelist context."""
|
303
|
+
extra_context = extra_context or {}
|
304
|
+
|
305
|
+
try:
|
306
|
+
# Basic statistics
|
307
|
+
total_balances = UserBalance.objects.count()
|
308
|
+
|
309
|
+
# Balance statistics
|
310
|
+
balance_stats = UserBalance.objects.aggregate(
|
311
|
+
total_balance=Sum('balance_usd'),
|
312
|
+
avg_balance=Avg('balance_usd'),
|
313
|
+
total_reserved=Sum('reserved_usd')
|
314
|
+
)
|
315
|
+
|
316
|
+
# Balance distribution
|
317
|
+
zero_balances = UserBalance.objects.filter(balance_usd=0).count()
|
318
|
+
negative_balances = UserBalance.objects.filter(balance_usd__lt=0).count()
|
319
|
+
low_balances = UserBalance.objects.filter(balance_usd__gt=0, balance_usd__lt=10).count()
|
320
|
+
medium_balances = UserBalance.objects.filter(balance_usd__gte=10, balance_usd__lt=100).count()
|
321
|
+
high_balances = UserBalance.objects.filter(balance_usd__gte=100, balance_usd__lt=1000).count()
|
322
|
+
whale_balances = UserBalance.objects.filter(balance_usd__gte=1000).count()
|
323
|
+
|
324
|
+
# Recent activity
|
325
|
+
recent_threshold = timezone.now() - timedelta(days=7)
|
326
|
+
active_balances = UserBalance.objects.filter(
|
327
|
+
last_transaction_at__gte=recent_threshold
|
328
|
+
).count()
|
329
|
+
|
330
|
+
# Top balances
|
331
|
+
top_balances = UserBalance.objects.filter(
|
332
|
+
balance_usd__gt=0
|
333
|
+
).order_by('-balance_usd')[:5]
|
334
|
+
|
335
|
+
extra_context.update({
|
336
|
+
'balance_stats': {
|
337
|
+
'total_balances': total_balances,
|
338
|
+
'total_balance': balance_stats['total_balance'] or 0,
|
339
|
+
'avg_balance': balance_stats['avg_balance'] or 0,
|
340
|
+
'total_reserved': balance_stats['total_reserved'] or 0,
|
341
|
+
'zero_balances': zero_balances,
|
342
|
+
'negative_balances': negative_balances,
|
343
|
+
'low_balances': low_balances,
|
344
|
+
'medium_balances': medium_balances,
|
345
|
+
'high_balances': high_balances,
|
346
|
+
'whale_balances': whale_balances,
|
347
|
+
'active_balances': active_balances,
|
348
|
+
'top_balances': top_balances,
|
349
|
+
}
|
350
|
+
})
|
351
|
+
|
352
|
+
except Exception as e:
|
353
|
+
logger.warning(f"Failed to generate balance statistics: {e}")
|
354
|
+
extra_context['balance_stats'] = None
|
355
|
+
|
356
|
+
return super().changelist_view(request, extra_context)
|
357
|
+
|
358
|
+
# ===== ADMIN ACTIONS =====
|
170
359
|
|
171
|
-
|
172
|
-
"
|
173
|
-
|
360
|
+
@action(
|
361
|
+
description="💰 Add Funds (Bulk)",
|
362
|
+
icon="add_circle",
|
363
|
+
variant=ActionVariant.SUCCESS
|
364
|
+
)
|
365
|
+
def add_funds_bulk(self, request, queryset):
|
366
|
+
"""Add funds to selected user balances."""
|
174
367
|
|
175
|
-
|
176
|
-
|
368
|
+
# This would typically show a form for amount input
|
369
|
+
# For now, we'll add a fixed amount as an example
|
370
|
+
amount = Decimal('10.00') # In production, this should come from a form
|
177
371
|
|
178
|
-
|
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
|
-
'''
|
372
|
+
updated_count = 0
|
198
373
|
|
199
|
-
|
200
|
-
|
374
|
+
for balance in queryset:
|
375
|
+
try:
|
376
|
+
# Use manager method for proper transaction handling
|
377
|
+
UserBalance.objects.add_funds_to_user(
|
378
|
+
user=balance.user,
|
379
|
+
amount=amount,
|
380
|
+
transaction_type='admin_adjustment',
|
381
|
+
description=f'Bulk funds addition by admin {request.user.username}'
|
382
|
+
)
|
383
|
+
updated_count += 1
|
384
|
+
|
385
|
+
except Exception as e:
|
386
|
+
logger.error(f"Failed to add funds to user {balance.user.id}: {e}")
|
201
387
|
|
202
|
-
|
203
|
-
|
388
|
+
if updated_count > 0:
|
389
|
+
messages.success(
|
390
|
+
request,
|
391
|
+
f"💰 Added ${amount} to {updated_count} user balances"
|
392
|
+
)
|
393
|
+
messages.info(
|
394
|
+
request,
|
395
|
+
"ℹ️ All transactions have been logged for audit purposes"
|
396
|
+
)
|
204
397
|
|
205
|
-
|
398
|
+
@action(
|
399
|
+
description="💸 Subtract Funds (Bulk)",
|
400
|
+
icon="remove_circle",
|
401
|
+
variant=ActionVariant.WARNING
|
402
|
+
)
|
403
|
+
def subtract_funds_bulk(self, request, queryset):
|
404
|
+
"""Subtract funds from selected user balances."""
|
405
|
+
|
406
|
+
amount = Decimal('5.00') # In production, this should come from a form
|
407
|
+
|
408
|
+
updated_count = 0
|
409
|
+
insufficient_funds = 0
|
410
|
+
|
411
|
+
for balance in queryset:
|
412
|
+
try:
|
413
|
+
if balance.balance_usd >= amount:
|
414
|
+
UserBalance.objects.subtract_funds_from_user(
|
415
|
+
user=balance.user,
|
416
|
+
amount=amount,
|
417
|
+
transaction_type='admin_adjustment',
|
418
|
+
description=f'Bulk funds subtraction by admin {request.user.username}'
|
419
|
+
)
|
420
|
+
updated_count += 1
|
421
|
+
else:
|
422
|
+
insufficient_funds += 1
|
423
|
+
|
424
|
+
except Exception as e:
|
425
|
+
logger.error(f"Failed to subtract funds from user {balance.user.id}: {e}")
|
426
|
+
|
427
|
+
if updated_count > 0:
|
428
|
+
messages.success(
|
429
|
+
request,
|
430
|
+
f"💸 Subtracted ${amount} from {updated_count} user balances"
|
431
|
+
)
|
432
|
+
|
433
|
+
if insufficient_funds > 0:
|
434
|
+
messages.warning(
|
435
|
+
request,
|
436
|
+
f"⚠️ Skipped {insufficient_funds} users with insufficient funds"
|
437
|
+
)
|
206
438
|
|
207
439
|
@action(
|
208
|
-
description="
|
209
|
-
icon="
|
210
|
-
variant=ActionVariant.
|
440
|
+
description="🔄 Reset Zero Balances",
|
441
|
+
icon="refresh",
|
442
|
+
variant=ActionVariant.INFO
|
211
443
|
)
|
212
|
-
def
|
213
|
-
"""
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
444
|
+
def reset_zero_balances(self, request, queryset):
|
445
|
+
"""Reset zero balances and clear reserved amounts."""
|
446
|
+
|
447
|
+
zero_balances = queryset.filter(balance_usd=0)
|
448
|
+
reset_count = 0
|
449
|
+
|
450
|
+
for balance in zero_balances:
|
451
|
+
if balance.reserved_usd and balance.reserved_usd > 0:
|
452
|
+
balance.reserved_usd = 0
|
453
|
+
balance.save(update_fields=['reserved_usd'])
|
454
|
+
reset_count += 1
|
455
|
+
|
456
|
+
if reset_count > 0:
|
457
|
+
messages.success(
|
218
458
|
request,
|
219
|
-
f"
|
220
|
-
|
459
|
+
f"🔄 Reset reserved amounts for {reset_count} zero balances"
|
460
|
+
)
|
461
|
+
else:
|
462
|
+
messages.info(
|
463
|
+
request,
|
464
|
+
"ℹ️ No zero balances with reserved amounts found"
|
221
465
|
)
|
222
|
-
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
223
466
|
|
224
467
|
@action(
|
225
|
-
description="📊
|
226
|
-
icon="
|
468
|
+
description="📊 Export Balance Report",
|
469
|
+
icon="download",
|
227
470
|
variant=ActionVariant.INFO
|
228
471
|
)
|
229
|
-
def
|
230
|
-
"""
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
472
|
+
def export_balance_report(self, request, queryset):
|
473
|
+
"""Export balance report to CSV."""
|
474
|
+
|
475
|
+
import csv
|
476
|
+
from django.http import HttpResponse
|
477
|
+
|
478
|
+
response = HttpResponse(content_type='text/csv')
|
479
|
+
response['Content-Disposition'] = f'attachment; filename="balance_report_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv"'
|
480
|
+
|
481
|
+
writer = csv.writer(response)
|
482
|
+
writer.writerow([
|
483
|
+
'User Email', 'User Name', 'Balance USD', 'Reserved USD', 'Available USD',
|
484
|
+
'Last Transaction', 'Created', 'Status'
|
485
|
+
])
|
486
|
+
|
487
|
+
for balance in queryset:
|
488
|
+
available = balance.balance_usd - (balance.reserved_usd or 0)
|
489
|
+
|
490
|
+
if balance.balance_usd < 0:
|
491
|
+
status = 'Negative'
|
492
|
+
elif balance.balance_usd == 0:
|
493
|
+
status = 'Empty'
|
494
|
+
elif balance.balance_usd < 10:
|
495
|
+
status = 'Low'
|
496
|
+
elif balance.balance_usd < 100:
|
497
|
+
status = 'Medium'
|
498
|
+
else:
|
499
|
+
status = 'High'
|
500
|
+
|
501
|
+
writer.writerow([
|
502
|
+
balance.user.email if balance.user else '',
|
503
|
+
balance.user.get_full_name() if balance.user else '',
|
504
|
+
balance.balance_usd,
|
505
|
+
balance.reserved_usd or 0,
|
506
|
+
available,
|
507
|
+
balance.last_transaction_at.isoformat() if balance.last_transaction_at else '',
|
508
|
+
balance.created_at.isoformat(),
|
509
|
+
status
|
510
|
+
])
|
511
|
+
|
512
|
+
messages.success(
|
513
|
+
request,
|
514
|
+
f"📊 Exported {queryset.count()} balance records to CSV"
|
515
|
+
)
|
516
|
+
|
517
|
+
return response
|
518
|
+
|
519
|
+
@action(
|
520
|
+
description="🔔 Send Low Balance Alerts",
|
521
|
+
icon="notifications",
|
522
|
+
variant=ActionVariant.WARNING
|
523
|
+
)
|
524
|
+
def send_low_balance_alerts(self, request, queryset):
|
525
|
+
"""Send alerts for low balance users."""
|
526
|
+
|
527
|
+
low_balance_users = queryset.filter(
|
528
|
+
balance_usd__gt=0,
|
529
|
+
balance_usd__lt=10
|
530
|
+
)
|
531
|
+
|
532
|
+
alert_count = 0
|
533
|
+
|
534
|
+
for balance in low_balance_users:
|
535
|
+
try:
|
536
|
+
# In production, this would send an actual notification
|
537
|
+
# For now, we'll just log it
|
538
|
+
logger.info(
|
539
|
+
f"Low balance alert for user {balance.user.email}: ${balance.balance_usd}"
|
540
|
+
)
|
541
|
+
alert_count += 1
|
542
|
+
|
543
|
+
except Exception as e:
|
544
|
+
logger.error(f"Failed to send alert to user {balance.user.id}: {e}")
|
545
|
+
|
546
|
+
if alert_count > 0:
|
547
|
+
messages.success(
|
548
|
+
request,
|
549
|
+
f"🔔 Sent low balance alerts to {alert_count} users"
|
550
|
+
)
|
551
|
+
else:
|
552
|
+
messages.info(
|
553
|
+
request,
|
554
|
+
"ℹ️ No users with low balances found in selection"
|
555
|
+
)
|
236
556
|
|
237
557
|
|
238
558
|
@admin.register(Transaction)
|
239
559
|
class TransactionAdmin(ModelAdmin):
|
240
|
-
"""
|
560
|
+
"""
|
561
|
+
Transaction admin with detailed tracking and audit capabilities.
|
562
|
+
|
563
|
+
Features:
|
564
|
+
- Comprehensive transaction history
|
565
|
+
- Financial audit trail
|
566
|
+
- Transaction type filtering
|
567
|
+
- Balance impact visualization
|
568
|
+
"""
|
241
569
|
|
242
570
|
list_display = [
|
243
|
-
'
|
571
|
+
'transaction_id_display',
|
244
572
|
'user_display',
|
573
|
+
'transaction_type_badge',
|
245
574
|
'amount_display',
|
246
|
-
'
|
247
|
-
'
|
248
|
-
'subscription_display',
|
575
|
+
'balance_impact_display',
|
576
|
+
'payment_link_display',
|
249
577
|
'created_at_display'
|
250
578
|
]
|
251
579
|
|
252
|
-
list_display_links = ['
|
580
|
+
list_display_links = ['transaction_id_display']
|
253
581
|
|
254
582
|
search_fields = [
|
583
|
+
'id',
|
255
584
|
'user__email',
|
585
|
+
'user__username',
|
256
586
|
'description',
|
257
|
-
'
|
258
|
-
'subscription__endpoint_group__name'
|
587
|
+
'payment_id'
|
259
588
|
]
|
260
589
|
|
261
590
|
list_filter = [
|
262
|
-
|
263
|
-
UserEmailFilter,
|
591
|
+
'transaction_type',
|
264
592
|
RecentActivityFilter,
|
265
|
-
'payment__status',
|
266
|
-
'subscription__status',
|
267
593
|
'created_at'
|
268
594
|
]
|
269
595
|
|
270
596
|
readonly_fields = [
|
271
|
-
'
|
272
|
-
'
|
273
|
-
'
|
597
|
+
'id',
|
598
|
+
'user',
|
599
|
+
'transaction_type',
|
600
|
+
'amount_usd',
|
601
|
+
'balance_after',
|
602
|
+
'payment_id',
|
603
|
+
'description',
|
604
|
+
'created_at'
|
274
605
|
]
|
275
606
|
|
276
|
-
|
277
|
-
(
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
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
|
-
]
|
607
|
+
def has_add_permission(self, request):
|
608
|
+
"""Disable adding transactions through admin (should be created by system)."""
|
609
|
+
return False
|
610
|
+
|
611
|
+
def has_change_permission(self, request, obj=None):
|
612
|
+
"""Disable changing transactions (audit trail integrity)."""
|
613
|
+
return False
|
297
614
|
|
298
|
-
|
299
|
-
|
300
|
-
|
615
|
+
def has_delete_permission(self, request, obj=None):
|
616
|
+
"""Disable deleting transactions (audit trail integrity)."""
|
617
|
+
return False
|
618
|
+
|
619
|
+
@display(description="Transaction ID", ordering='id')
|
620
|
+
def transaction_id_display(self, obj):
|
621
|
+
"""Display transaction ID."""
|
622
|
+
short_id = str(obj.id)[:8]
|
301
623
|
return format_html(
|
302
|
-
'<
|
303
|
-
|
304
|
-
obj.
|
624
|
+
'<span class="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded" '
|
625
|
+
'title="Full ID: {}">{}</span>',
|
626
|
+
obj.id,
|
627
|
+
short_id
|
305
628
|
)
|
306
629
|
|
307
|
-
@display(description="User")
|
630
|
+
@display(description="User", ordering='user__email')
|
308
631
|
def user_display(self, obj):
|
309
632
|
"""Display user information."""
|
633
|
+
if obj.user:
|
634
|
+
return format_html(
|
635
|
+
'<div>'
|
636
|
+
'<div class="font-medium">{}</div>'
|
637
|
+
'<div class="text-xs text-gray-500">{}</div>'
|
638
|
+
'</div>',
|
639
|
+
obj.user.get_full_name() or obj.user.username,
|
640
|
+
obj.user.email
|
641
|
+
)
|
642
|
+
return format_html('<span class="text-gray-500">No user</span>')
|
643
|
+
|
644
|
+
@display(description="Type", ordering='transaction_type')
|
645
|
+
def transaction_type_badge(self, obj):
|
646
|
+
"""Display transaction type with colored badge."""
|
647
|
+
type_config = {
|
648
|
+
'payment': ('💳', 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', 'Payment'),
|
649
|
+
'deposit': ('💰', 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200', 'Deposit'),
|
650
|
+
'withdrawal': ('💸', 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', 'Withdrawal'),
|
651
|
+
'refund': ('↩️', 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200', 'Refund'),
|
652
|
+
'admin_adjustment': ('⚙️', 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200', 'Admin'),
|
653
|
+
'fee': ('📋', 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200', 'Fee'),
|
654
|
+
}
|
655
|
+
|
656
|
+
icon, color_class, label = type_config.get(
|
657
|
+
obj.transaction_type,
|
658
|
+
('❓', 'bg-gray-100 text-gray-800', obj.transaction_type.title())
|
659
|
+
)
|
660
|
+
|
310
661
|
return format_html(
|
311
|
-
'<
|
312
|
-
|
313
|
-
|
662
|
+
'<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {}">'
|
663
|
+
'{} {}'
|
664
|
+
'</span>',
|
665
|
+
color_class,
|
666
|
+
icon,
|
667
|
+
label
|
314
668
|
)
|
315
669
|
|
316
|
-
@display(description="Amount")
|
670
|
+
@display(description="Amount", ordering='amount_usd')
|
317
671
|
def amount_display(self, obj):
|
318
|
-
"""Display amount with
|
672
|
+
"""Display transaction amount with sign."""
|
319
673
|
amount = obj.amount_usd
|
320
|
-
color = '#28a745' if amount > 0 else '#dc3545'
|
321
|
-
sign = '+' if amount > 0 else ''
|
322
674
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
675
|
+
if amount > 0:
|
676
|
+
return format_html(
|
677
|
+
'<span class="font-bold text-green-600 dark:text-green-400">+${:,.2f}</span>',
|
678
|
+
amount
|
679
|
+
)
|
680
|
+
elif amount < 0:
|
681
|
+
return format_html(
|
682
|
+
'<span class="font-bold text-red-600 dark:text-red-400">-${:,.2f}</span>',
|
683
|
+
abs(amount)
|
684
|
+
)
|
685
|
+
else:
|
686
|
+
return format_html(
|
687
|
+
'<span class="font-bold text-gray-600 dark:text-gray-400">${:,.2f}</span>',
|
688
|
+
amount
|
689
|
+
)
|
328
690
|
|
329
|
-
@display(description="
|
330
|
-
def
|
331
|
-
"""Display
|
332
|
-
|
333
|
-
|
334
|
-
'debit': '#dc3545',
|
335
|
-
'refund': '#17a2b8',
|
336
|
-
'withdrawal': '#ffc107',
|
337
|
-
}
|
338
|
-
|
339
|
-
color = type_colors.get(obj.transaction_type, '#6c757d')
|
691
|
+
@display(description="Balance Impact")
|
692
|
+
def balance_impact_display(self, obj):
|
693
|
+
"""Display balance before/after transaction."""
|
694
|
+
# Calculate balance_before from balance_after and amount_usd
|
695
|
+
balance_before = obj.balance_after - obj.amount_usd
|
340
696
|
|
341
697
|
return format_html(
|
342
|
-
'<
|
343
|
-
|
344
|
-
|
698
|
+
'<div class="text-xs">'
|
699
|
+
'<div>Before: <span class="font-mono">${:,.2f}</span></div>'
|
700
|
+
'<div>After: <span class="font-mono">${:,.2f}</span></div>'
|
701
|
+
'</div>',
|
702
|
+
balance_before,
|
703
|
+
obj.balance_after
|
345
704
|
)
|
346
705
|
|
347
706
|
@display(description="Payment")
|
348
|
-
def
|
349
|
-
"""Display
|
350
|
-
if obj.
|
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:
|
707
|
+
def payment_link_display(self, obj):
|
708
|
+
"""Display payment link if available."""
|
709
|
+
if obj.payment_id:
|
363
710
|
return format_html(
|
364
|
-
'<a href="{}"
|
365
|
-
|
366
|
-
|
367
|
-
|
711
|
+
'<a href="/admin/payments/universalpayment/{}/change/" '
|
712
|
+
'class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">'
|
713
|
+
'🔗 Payment'
|
714
|
+
'</a>',
|
715
|
+
obj.payment_id
|
368
716
|
)
|
369
|
-
return "
|
717
|
+
return format_html('<span class="text-gray-500">—</span>')
|
370
718
|
|
371
|
-
@display(description="Created")
|
719
|
+
@display(description="Created", ordering='created_at')
|
372
720
|
def created_at_display(self, obj):
|
373
|
-
"""Display creation
|
374
|
-
return naturaltime(obj.created_at)
|
375
|
-
|
376
|
-
def transaction_details(self, obj):
|
377
|
-
"""Show detailed transaction information."""
|
721
|
+
"""Display creation timestamp."""
|
378
722
|
return format_html(
|
379
|
-
'<div
|
380
|
-
'<
|
381
|
-
'
|
382
|
-
'• User: {} ({})<br>'
|
383
|
-
'• Type: {}<br>'
|
384
|
-
'• Amount: <span style="color: {};">${:.2f}</span><br>'
|
385
|
-
'• Description: {}<br>'
|
386
|
-
'• Created: {}<br>'
|
387
|
-
'{}'
|
388
|
-
'{}'
|
723
|
+
'<div class="text-xs">'
|
724
|
+
'<div>{}</div>'
|
725
|
+
'<div class="text-gray-500">{}</div>'
|
389
726
|
'</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
727
|
obj.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
398
|
-
|
399
|
-
f'• Subscription: {obj.subscription.endpoint_group.name}<br>' if obj.subscription else ''
|
728
|
+
naturaltime(obj.created_at)
|
400
729
|
)
|
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"
|