django-cfg 1.2.31__py3-none-any.whl → 1.3.3__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 (264) 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 -10
  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 +526 -222
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +465 -70
  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/cleanup_expired_data.py +419 -0
  39. django_cfg/apps/payments/management/commands/currency_stats.py +297 -225
  40. django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
  41. django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
  42. django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
  43. django_cfg/apps/payments/management/commands/test_providers.py +434 -0
  44. django_cfg/apps/payments/middleware/__init__.py +3 -1
  45. django_cfg/apps/payments/middleware/api_access.py +329 -222
  46. django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
  47. django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
  48. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  49. django_cfg/apps/payments/models/__init__.py +13 -18
  50. django_cfg/apps/payments/models/api_keys.py +121 -43
  51. django_cfg/apps/payments/models/balance.py +153 -115
  52. django_cfg/apps/payments/models/base.py +68 -15
  53. django_cfg/apps/payments/models/currencies.py +172 -148
  54. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  55. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  56. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  57. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  58. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  59. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  60. django_cfg/apps/payments/models/payments.py +235 -285
  61. django_cfg/apps/payments/models/subscriptions.py +257 -177
  62. django_cfg/apps/payments/models/tariffs.py +147 -40
  63. django_cfg/apps/payments/services/__init__.py +209 -56
  64. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  65. django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
  66. django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
  67. django_cfg/apps/payments/services/{cache/base.py → cache_service/interfaces.py} +3 -1
  68. django_cfg/apps/payments/services/cache_service/keys.py +49 -0
  69. django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
  70. django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
  71. django_cfg/apps/payments/services/core/__init__.py +10 -6
  72. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  73. django_cfg/apps/payments/services/core/base.py +166 -0
  74. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  75. django_cfg/apps/payments/services/core/payment_service.py +371 -465
  76. django_cfg/apps/payments/services/core/subscription_service.py +425 -481
  77. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  78. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  79. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  80. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  81. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  82. django_cfg/apps/payments/services/providers/base.py +234 -174
  83. django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
  84. django_cfg/apps/payments/services/providers/registry.py +367 -301
  85. django_cfg/apps/payments/services/types/__init__.py +78 -0
  86. django_cfg/apps/payments/services/types/data.py +177 -0
  87. django_cfg/apps/payments/services/types/requests.py +150 -0
  88. django_cfg/apps/payments/services/types/responses.py +156 -0
  89. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  90. django_cfg/apps/payments/signals/__init__.py +33 -8
  91. django_cfg/apps/payments/signals/api_key_signals.py +210 -129
  92. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  93. django_cfg/apps/payments/signals/payment_signals.py +128 -103
  94. django_cfg/apps/payments/signals/subscription_signals.py +194 -142
  95. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  96. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  97. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  98. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  99. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  100. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  101. django_cfg/apps/payments/urls.py +45 -48
  102. django_cfg/apps/payments/urls_admin.py +33 -42
  103. django_cfg/apps/payments/views/api/__init__.py +101 -0
  104. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  105. django_cfg/apps/payments/views/api/balances.py +381 -0
  106. django_cfg/apps/payments/views/api/base.py +298 -0
  107. django_cfg/apps/payments/views/api/currencies.py +402 -0
  108. django_cfg/apps/payments/views/api/payments.py +415 -0
  109. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  110. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  111. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  112. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  113. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  114. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  115. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  116. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  117. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  118. django_cfg/config.py +1 -1
  119. django_cfg/core/config.py +40 -4
  120. django_cfg/core/generation.py +25 -4
  121. django_cfg/core/integration/README.md +363 -0
  122. django_cfg/core/integration/__init__.py +47 -0
  123. django_cfg/core/integration/commands_collector.py +239 -0
  124. django_cfg/core/integration/display/__init__.py +15 -0
  125. django_cfg/core/integration/display/base.py +157 -0
  126. django_cfg/core/integration/display/ngrok.py +164 -0
  127. django_cfg/core/integration/display/startup.py +815 -0
  128. django_cfg/core/integration/url_integration.py +123 -0
  129. django_cfg/core/integration/version_checker.py +160 -0
  130. django_cfg/management/commands/auto_generate.py +4 -0
  131. django_cfg/management/commands/check_settings.py +6 -0
  132. django_cfg/management/commands/clear_constance.py +5 -2
  133. django_cfg/management/commands/create_token.py +6 -0
  134. django_cfg/management/commands/list_urls.py +6 -0
  135. django_cfg/management/commands/migrate_all.py +6 -0
  136. django_cfg/management/commands/migrator.py +3 -0
  137. django_cfg/management/commands/rundramatiq.py +6 -0
  138. django_cfg/management/commands/runserver_ngrok.py +51 -29
  139. django_cfg/management/commands/script.py +6 -0
  140. django_cfg/management/commands/show_config.py +12 -2
  141. django_cfg/management/commands/show_urls.py +4 -0
  142. django_cfg/management/commands/superuser.py +6 -0
  143. django_cfg/management/commands/task_clear.py +4 -1
  144. django_cfg/management/commands/task_status.py +3 -1
  145. django_cfg/management/commands/test_email.py +3 -0
  146. django_cfg/management/commands/test_telegram.py +6 -0
  147. django_cfg/management/commands/test_twilio.py +6 -0
  148. django_cfg/management/commands/tree.py +6 -0
  149. django_cfg/management/commands/validate_config.py +155 -149
  150. django_cfg/models/constance.py +31 -11
  151. django_cfg/models/payments.py +175 -492
  152. django_cfg/modules/django_logger.py +160 -146
  153. django_cfg/modules/django_unfold/dashboard.py +64 -16
  154. django_cfg/registry/core.py +1 -0
  155. django_cfg/template_archive/django_sample.zip +0 -0
  156. django_cfg/utils/smart_defaults.py +227 -570
  157. django_cfg/utils/toolkit.py +51 -11
  158. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/METADATA +4 -1
  159. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/RECORD +162 -185
  160. django_cfg/apps/payments/__init__.py +0 -8
  161. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  162. django_cfg/apps/payments/config/module.py +0 -70
  163. django_cfg/apps/payments/config/providers.py +0 -105
  164. django_cfg/apps/payments/config/settings.py +0 -96
  165. django_cfg/apps/payments/config/utils.py +0 -52
  166. django_cfg/apps/payments/decorators.py +0 -291
  167. django_cfg/apps/payments/management/commands/README.md +0 -146
  168. django_cfg/apps/payments/managers/__init__.py +0 -23
  169. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  170. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  171. django_cfg/apps/payments/managers/currency_manager.py +0 -306
  172. django_cfg/apps/payments/managers/payment_manager.py +0 -192
  173. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  174. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  175. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
  176. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
  177. django_cfg/apps/payments/models/events.py +0 -73
  178. django_cfg/apps/payments/serializers/__init__.py +0 -57
  179. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  180. django_cfg/apps/payments/serializers/balance.py +0 -59
  181. django_cfg/apps/payments/serializers/currencies.py +0 -63
  182. django_cfg/apps/payments/serializers/payments.py +0 -62
  183. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  184. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  185. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  186. django_cfg/apps/payments/services/cache/simple_cache.py +0 -135
  187. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  188. django_cfg/apps/payments/services/internal_types.py +0 -461
  189. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  190. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  191. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
  192. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  193. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
  194. django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
  195. django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
  196. django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
  197. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
  198. django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
  199. django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
  200. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
  201. django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
  202. django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
  203. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
  204. django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
  205. django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
  206. django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
  207. django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
  208. django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
  209. django_cfg/apps/payments/services/security/__init__.py +0 -34
  210. django_cfg/apps/payments/services/security/error_handler.py +0 -635
  211. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  212. django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
  213. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  214. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  215. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  216. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  217. django_cfg/apps/payments/tasks/__init__.py +0 -12
  218. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  219. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
  220. django_cfg/apps/payments/templates/payments/base.html +0 -182
  221. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  222. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  223. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
  224. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  225. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
  226. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
  227. django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
  228. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
  229. django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
  230. django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
  231. django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
  232. django_cfg/apps/payments/templates/payments/stats.html +0 -261
  233. django_cfg/apps/payments/templates/payments/test.html +0 -213
  234. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  235. django_cfg/apps/payments/utils/__init__.py +0 -43
  236. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  237. django_cfg/apps/payments/utils/config_utils.py +0 -239
  238. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  239. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  240. django_cfg/apps/payments/views/__init__.py +0 -63
  241. django_cfg/apps/payments/views/api_key_views.py +0 -164
  242. django_cfg/apps/payments/views/balance_views.py +0 -75
  243. django_cfg/apps/payments/views/currency_views.py +0 -122
  244. django_cfg/apps/payments/views/payment_views.py +0 -149
  245. django_cfg/apps/payments/views/subscription_views.py +0 -135
  246. django_cfg/apps/payments/views/tariff_views.py +0 -131
  247. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  248. django_cfg/apps/payments/views/templates/ajax.py +0 -451
  249. django_cfg/apps/payments/views/templates/base.py +0 -212
  250. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  251. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  252. django_cfg/apps/payments/views/templates/payment_management.py +0 -158
  253. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  254. django_cfg/apps/payments/views/templates/stats.py +0 -244
  255. django_cfg/apps/payments/views/templates/utils.py +0 -181
  256. django_cfg/apps/payments/views/webhook_views.py +0 -266
  257. django_cfg/apps/payments/viewsets.py +0 -66
  258. django_cfg/core/integration.py +0 -160
  259. django_cfg/template_archive/.gitignore +0 -1
  260. django_cfg/template_archive/__init__.py +0 -0
  261. django_cfg/urls.py +0 -33
  262. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/WHEEL +0 -0
  263. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/entry_points.txt +0 -0
  264. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.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;">${}</span>',
104
- color, f"{float(amount):.2f}"
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;">${}</span>',
113
- f"{float(obj.reserved_usd):.2f}"
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;">${}</span>',
123
- f"{float(available):.2f}"
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: {};">{} ${}</span><br><small>{}</small>',
133
- '#28a745' if last_transaction.amount_usd > 0 else '#dc3545',
134
- '+' if last_transaction.amount_usd > 0 else '',
135
- f"{float(abs(last_transaction.amount_usd)):.2f}",
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;">${}</span><br>'
156
- '• Total Debited: <span style="color: #dc3545;">${}</span><br>'
157
- '• Net Balance: <span style="color: {};">${}</span><br>'
158
- '• Total Transactions: {}<br>'
159
- '• Available Balance: <strong>${}</strong>'
293
+ '<div class="text-xs">'
294
+ '<div>{}</div>'
295
+ '<div class="text-gray-500">{}</div>'
160
296
  '</div>',
161
- f"{float(total_credited):.2f}",
162
- f"{float(total_debited):.2f}",
163
- '#28a745' if (total_credited - total_debited) > 0 else '#dc3545',
164
- f"{float(total_credited - total_debited):.2f}",
165
- transaction_count,
166
- f"{float(obj.amount_usd - obj.reserved_usd):.2f}"
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: {};">${}</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
- f"{float(obj.amount_usd):.2f}",
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"