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.
Files changed (258) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/health/views.py +4 -2
  3. django_cfg/apps/knowbase/config/settings.py +16 -15
  4. django_cfg/apps/payments/README.md +326 -0
  5. django_cfg/apps/payments/admin/__init__.py +20 -9
  6. django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
  7. django_cfg/apps/payments/admin/balance_admin.py +592 -297
  8. django_cfg/apps/payments/admin/currencies_admin.py +600 -108
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +470 -64
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
  39. django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
  43. django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +16 -20
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +207 -67
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -284
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +344 -468
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -484
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +232 -71
  74. django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
  75. django_cfg/apps/payments/services/providers/registry.py +429 -80
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +211 -130
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +129 -98
  85. django_cfg/apps/payments/signals/subscription_signals.py +195 -143
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +46 -47
  93. django_cfg/apps/payments/urls_admin.py +49 -0
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/apps/tasks/urls.py +0 -2
  110. django_cfg/apps/tasks/urls_admin.py +14 -0
  111. django_cfg/apps/urls.py +4 -4
  112. django_cfg/config.py +1 -1
  113. django_cfg/core/config.py +75 -4
  114. django_cfg/core/generation.py +25 -4
  115. django_cfg/core/integration/README.md +363 -0
  116. django_cfg/core/integration/__init__.py +47 -0
  117. django_cfg/core/integration/commands_collector.py +239 -0
  118. django_cfg/core/integration/display/__init__.py +15 -0
  119. django_cfg/core/integration/display/base.py +157 -0
  120. django_cfg/core/integration/display/ngrok.py +164 -0
  121. django_cfg/core/integration/display/startup.py +815 -0
  122. django_cfg/core/integration/url_integration.py +123 -0
  123. django_cfg/core/integration/version_checker.py +160 -0
  124. django_cfg/management/commands/auto_generate.py +4 -0
  125. django_cfg/management/commands/check_settings.py +6 -0
  126. django_cfg/management/commands/clear_constance.py +5 -2
  127. django_cfg/management/commands/create_token.py +6 -0
  128. django_cfg/management/commands/list_urls.py +6 -0
  129. django_cfg/management/commands/migrate_all.py +6 -0
  130. django_cfg/management/commands/migrator.py +3 -0
  131. django_cfg/management/commands/rundramatiq.py +6 -0
  132. django_cfg/management/commands/runserver_ngrok.py +51 -29
  133. django_cfg/management/commands/script.py +6 -0
  134. django_cfg/management/commands/show_config.py +12 -2
  135. django_cfg/management/commands/show_urls.py +4 -0
  136. django_cfg/management/commands/superuser.py +6 -0
  137. django_cfg/management/commands/task_clear.py +4 -1
  138. django_cfg/management/commands/task_status.py +3 -1
  139. django_cfg/management/commands/test_email.py +3 -0
  140. django_cfg/management/commands/test_telegram.py +6 -0
  141. django_cfg/management/commands/test_twilio.py +6 -0
  142. django_cfg/management/commands/tree.py +6 -0
  143. django_cfg/management/commands/validate_config.py +155 -149
  144. django_cfg/models/constance.py +31 -11
  145. django_cfg/models/payments.py +175 -498
  146. django_cfg/modules/django_currency/__init__.py +16 -11
  147. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  148. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  149. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  150. django_cfg/modules/django_currency/core/__init__.py +1 -7
  151. django_cfg/modules/django_currency/core/converter.py +18 -23
  152. django_cfg/modules/django_currency/core/models.py +122 -11
  153. django_cfg/modules/django_currency/database/__init__.py +4 -4
  154. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  155. django_cfg/modules/django_logger.py +160 -146
  156. django_cfg/modules/django_unfold/dashboard.py +65 -12
  157. django_cfg/registry/core.py +1 -0
  158. django_cfg/template_archive/django_sample.zip +0 -0
  159. django_cfg/templates/admin/components/action_grid.html +9 -9
  160. django_cfg/templates/admin/components/metric_card.html +5 -5
  161. django_cfg/templates/admin/components/status_badge.html +2 -2
  162. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  163. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  164. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  165. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  166. django_cfg/utils/smart_defaults.py +222 -571
  167. django_cfg/utils/toolkit.py +51 -11
  168. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
  169. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
  170. django_cfg/apps/payments/__init__.py +0 -8
  171. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  172. django_cfg/apps/payments/config/module.py +0 -70
  173. django_cfg/apps/payments/config/providers.py +0 -105
  174. django_cfg/apps/payments/config/settings.py +0 -96
  175. django_cfg/apps/payments/config/utils.py +0 -52
  176. django_cfg/apps/payments/decorators.py +0 -291
  177. django_cfg/apps/payments/management/commands/README.md +0 -178
  178. django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
  179. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  180. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  181. django_cfg/apps/payments/managers/__init__.py +0 -22
  182. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  183. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  184. django_cfg/apps/payments/managers/currency_manager.py +0 -83
  185. django_cfg/apps/payments/managers/payment_manager.py +0 -44
  186. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  187. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  188. django_cfg/apps/payments/models/events.py +0 -73
  189. django_cfg/apps/payments/serializers/__init__.py +0 -56
  190. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  191. django_cfg/apps/payments/serializers/balance.py +0 -59
  192. django_cfg/apps/payments/serializers/currencies.py +0 -55
  193. django_cfg/apps/payments/serializers/payments.py +0 -62
  194. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  195. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  196. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  197. django_cfg/apps/payments/services/cache/base.py +0 -30
  198. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  199. django_cfg/apps/payments/services/internal_types.py +0 -297
  200. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  201. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  202. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
  203. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  204. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  205. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  206. django_cfg/apps/payments/services/security/__init__.py +0 -34
  207. django_cfg/apps/payments/services/security/error_handler.py +0 -637
  208. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  209. django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
  210. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  211. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  212. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  213. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  214. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  215. django_cfg/apps/payments/tasks/__init__.py +0 -12
  216. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  217. django_cfg/apps/payments/templates/payments/base.html +0 -182
  218. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  219. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  220. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
  221. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  222. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
  223. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
  224. django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
  225. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  226. django_cfg/apps/payments/urls_templates.py +0 -52
  227. django_cfg/apps/payments/utils/__init__.py +0 -45
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -245
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -62
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -111
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -312
  241. django_cfg/apps/payments/views/templates/base.py +0 -204
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -164
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -240
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -65
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  252. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  253. django_cfg/template_archive/.gitignore +0 -1
  254. django_cfg/template_archive/__init__.py +0 -0
  255. django_cfg/urls.py +0 -33
  256. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  257. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  258. {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 for balance and transaction management.
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.urls import reverse
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, TransactionTypeFilter, UserEmailFilter, RecentActivityFilter
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
- """Admin interface for user balances."""
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
- 'reserved_display',
26
- 'available_display',
27
- 'last_transaction_display',
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 = ['user__email', 'user__first_name', 'user__last_name']
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
- 'transaction_history',
46
- 'balance_statistics'
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': ['amount_usd', 'reserved_usd']
55
- }),
56
- ('Statistics', {
57
- 'fields': ['balance_statistics'],
58
- 'classes': ['collapse']
90
+ 'fields': [
91
+ 'balance_usd',
92
+ 'reserved_usd'
93
+ ]
59
94
  }),
60
- ('Transaction History', {
61
- 'fields': ['transaction_history'],
62
- 'classes': ['collapse']
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
- actions_detail = ['add_funds', 'view_transactions']
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
- 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;" />'
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
- 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>'
181
+ balance_color = "text-blue-600 dark:text-blue-400"
182
+ balance_icon = "💎"
81
183
 
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
- )
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="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
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
- color = '#dc3545' # Red
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
- return format_html(
103
- '<span style="color: {}; font-weight: bold;">${:.2f}</span>',
104
- color, amount
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="Reserved")
108
- def reserved_display(self, obj):
109
- """Display reserved amount."""
110
- if obj.reserved_usd > 0:
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
- '<span style="color: #6c757d;">${:.2f}</span>',
113
- obj.reserved_usd
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
- return "—"
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
- '<span style="font-weight: bold;">${:.2f}</span>',
123
- available
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 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:
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
- '<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)
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
- return "No transactions"
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 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>'
293
+ '<div class="text-xs">'
294
+ '<div>{}</div>'
295
+ '<div class="text-gray-500">{}</div>'
160
296
  '</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
297
+ obj.created_at.strftime('%Y-%m-%d'),
298
+ naturaltime(obj.created_at)
167
299
  )
168
300
 
169
- balance_statistics.short_description = "Balance Statistics"
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
- def transaction_history(self, obj):
172
- """Show recent transaction history."""
173
- transactions = obj.user.transactions.order_by('-created_at')[:10]
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
- if not transactions:
176
- return "No transactions"
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
- 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
- '''
372
+ updated_count = 0
198
373
 
199
- if obj.user.transactions.count() > 10:
200
- html += f'<p><small><em>... and {obj.user.transactions.count() - 10} more transactions</em></small></p>'
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
- html += '</div>'
203
- return format_html(html)
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
- transaction_history.short_description = "Recent Transactions"
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="💰 Add Funds",
209
- icon="attach_money",
210
- variant=ActionVariant.SUCCESS
440
+ description="🔄 Reset Zero Balances",
441
+ icon="refresh",
442
+ variant=ActionVariant.INFO
211
443
  )
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(
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"Add funds form would open for {balance.user.email}",
220
- level='info'
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="📊 View Transactions",
226
- icon="receipt_long",
468
+ description="📊 Export Balance Report",
469
+ icon="download",
227
470
  variant=ActionVariant.INFO
228
471
  )
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/'))
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
- """Admin interface for transactions."""
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
- 'transaction_display',
571
+ 'transaction_id_display',
244
572
  'user_display',
573
+ 'transaction_type_badge',
245
574
  'amount_display',
246
- 'type_display',
247
- 'payment_display',
248
- 'subscription_display',
575
+ 'balance_impact_display',
576
+ 'payment_link_display',
249
577
  'created_at_display'
250
578
  ]
251
579
 
252
- list_display_links = ['transaction_display']
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
- 'payment__internal_payment_id',
258
- 'subscription__endpoint_group__name'
587
+ 'payment_id'
259
588
  ]
260
589
 
261
590
  list_filter = [
262
- TransactionTypeFilter,
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
- 'created_at',
272
- 'transaction_details',
273
- 'related_objects'
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
- 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
- ]
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
- @display(description="Transaction")
299
- def transaction_display(self, obj):
300
- """Display transaction ID and description."""
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
- '<strong>#{}</strong><br><small>{}</small>',
303
- str(obj.id)[:8],
304
- obj.description[:40] + '...' if len(obj.description) > 40 else obj.description
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
- '<strong>{}</strong><br><small>{}</small>',
312
- obj.user.get_full_name() or obj.user.email,
313
- obj.user.email
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 color coding."""
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
- return format_html(
324
- '<span style="color: {}; font-weight: bold; font-size: 14px;">{}</span>',
325
- color,
326
- f'{sign}${abs(amount):.2f}'
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="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')
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
- '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>',
343
- color,
344
- obj.get_transaction_type_display()
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 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:
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="{}" 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()
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 date."""
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 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
- '{}'
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
- 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 ''
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"