django-cfg 1.2.31__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 (256) 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/manage_currencies.py +303 -151
  39. django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
  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 +342 -152
  43. django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +13 -18
  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 +172 -148
  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 -285
  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 +346 -467
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -481
  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 +234 -174
  74. django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
  75. django_cfg/apps/payments/services/providers/registry.py +367 -301
  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 +210 -129
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +128 -103
  85. django_cfg/apps/payments/signals/subscription_signals.py +194 -142
  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 +45 -48
  93. django_cfg/apps/payments/urls_admin.py +33 -42
  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/config.py +1 -1
  110. django_cfg/core/config.py +40 -4
  111. django_cfg/core/generation.py +25 -4
  112. django_cfg/core/integration/README.md +363 -0
  113. django_cfg/core/integration/__init__.py +47 -0
  114. django_cfg/core/integration/commands_collector.py +239 -0
  115. django_cfg/core/integration/display/__init__.py +15 -0
  116. django_cfg/core/integration/display/base.py +157 -0
  117. django_cfg/core/integration/display/ngrok.py +164 -0
  118. django_cfg/core/integration/display/startup.py +815 -0
  119. django_cfg/core/integration/url_integration.py +123 -0
  120. django_cfg/core/integration/version_checker.py +160 -0
  121. django_cfg/management/commands/auto_generate.py +4 -0
  122. django_cfg/management/commands/check_settings.py +6 -0
  123. django_cfg/management/commands/clear_constance.py +5 -2
  124. django_cfg/management/commands/create_token.py +6 -0
  125. django_cfg/management/commands/list_urls.py +6 -0
  126. django_cfg/management/commands/migrate_all.py +6 -0
  127. django_cfg/management/commands/migrator.py +3 -0
  128. django_cfg/management/commands/rundramatiq.py +6 -0
  129. django_cfg/management/commands/runserver_ngrok.py +51 -29
  130. django_cfg/management/commands/script.py +6 -0
  131. django_cfg/management/commands/show_config.py +12 -2
  132. django_cfg/management/commands/show_urls.py +4 -0
  133. django_cfg/management/commands/superuser.py +6 -0
  134. django_cfg/management/commands/task_clear.py +4 -1
  135. django_cfg/management/commands/task_status.py +3 -1
  136. django_cfg/management/commands/test_email.py +3 -0
  137. django_cfg/management/commands/test_telegram.py +6 -0
  138. django_cfg/management/commands/test_twilio.py +6 -0
  139. django_cfg/management/commands/tree.py +6 -0
  140. django_cfg/management/commands/validate_config.py +155 -149
  141. django_cfg/models/constance.py +31 -11
  142. django_cfg/models/payments.py +175 -492
  143. django_cfg/modules/django_logger.py +160 -146
  144. django_cfg/modules/django_unfold/dashboard.py +64 -16
  145. django_cfg/registry/core.py +1 -0
  146. django_cfg/template_archive/django_sample.zip +0 -0
  147. django_cfg/utils/smart_defaults.py +222 -571
  148. django_cfg/utils/toolkit.py +51 -11
  149. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
  150. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
  151. django_cfg/apps/payments/__init__.py +0 -8
  152. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  153. django_cfg/apps/payments/config/module.py +0 -70
  154. django_cfg/apps/payments/config/providers.py +0 -105
  155. django_cfg/apps/payments/config/settings.py +0 -96
  156. django_cfg/apps/payments/config/utils.py +0 -52
  157. django_cfg/apps/payments/decorators.py +0 -291
  158. django_cfg/apps/payments/management/commands/README.md +0 -146
  159. django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
  160. django_cfg/apps/payments/managers/__init__.py +0 -23
  161. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  162. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  163. django_cfg/apps/payments/managers/currency_manager.py +0 -306
  164. django_cfg/apps/payments/managers/payment_manager.py +0 -192
  165. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  166. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  167. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
  168. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
  169. django_cfg/apps/payments/models/events.py +0 -73
  170. django_cfg/apps/payments/serializers/__init__.py +0 -57
  171. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  172. django_cfg/apps/payments/serializers/balance.py +0 -59
  173. django_cfg/apps/payments/serializers/currencies.py +0 -63
  174. django_cfg/apps/payments/serializers/payments.py +0 -62
  175. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  176. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  177. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  178. django_cfg/apps/payments/services/cache/base.py +0 -30
  179. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  180. django_cfg/apps/payments/services/internal_types.py +0 -461
  181. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  182. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  183. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
  184. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  185. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
  186. django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
  187. django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
  188. django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
  189. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
  190. django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
  191. django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
  192. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
  193. django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
  194. django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
  195. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
  196. django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
  197. django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
  198. django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
  199. django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
  200. django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
  201. django_cfg/apps/payments/services/security/__init__.py +0 -34
  202. django_cfg/apps/payments/services/security/error_handler.py +0 -635
  203. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  204. django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
  205. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  206. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  207. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  208. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  209. django_cfg/apps/payments/tasks/__init__.py +0 -12
  210. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  211. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
  212. django_cfg/apps/payments/templates/payments/base.html +0 -182
  213. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  214. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  215. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
  216. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  217. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
  218. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
  219. django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
  220. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
  221. django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
  222. django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
  223. django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
  224. django_cfg/apps/payments/templates/payments/stats.html +0 -261
  225. django_cfg/apps/payments/templates/payments/test.html +0 -213
  226. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  227. django_cfg/apps/payments/utils/__init__.py +0 -43
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -239
  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 -63
  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 -122
  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 -451
  241. django_cfg/apps/payments/views/templates/base.py +0 -212
  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 -158
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -244
  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 -66
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/template_archive/.gitignore +0 -1
  252. django_cfg/template_archive/__init__.py +0 -0
  253. django_cfg/urls.py +0 -33
  254. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  255. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  256. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,381 @@
1
+ """
2
+ Balance ViewSets for the Universal Payment System v2.0.
3
+
4
+ DRF ViewSets for balance and transaction management with service integration.
5
+ """
6
+
7
+ from rest_framework import viewsets, permissions, status
8
+ from rest_framework.decorators import action
9
+ from rest_framework.response import Response
10
+ from django_filters.rest_framework import DjangoFilterBackend
11
+ from django.contrib.auth import get_user_model
12
+ from django.db import models
13
+
14
+ from .base import PaymentBaseViewSet, NestedPaymentViewSet, ReadOnlyPaymentViewSet
15
+ from ...models import UserBalance, Transaction
16
+ from ...services import get_balance_service
17
+ from ..serializers.balances import (
18
+ UserBalanceSerializer,
19
+ TransactionSerializer,
20
+ BalanceUpdateSerializer,
21
+ FundsTransferSerializer,
22
+ BalanceStatsSerializer,
23
+ )
24
+ from django_cfg.modules.django_logger import get_logger
25
+
26
+ User = get_user_model()
27
+ logger = get_logger("balance_viewsets")
28
+
29
+
30
+ class UserBalanceViewSet(ReadOnlyPaymentViewSet):
31
+ """
32
+ User balance ViewSet: /api/balances/
33
+
34
+ Read-only access to user balances with statistics.
35
+ """
36
+
37
+ queryset = UserBalance.objects.all()
38
+ serializer_class = UserBalanceSerializer
39
+ permission_classes = [permissions.IsAuthenticated]
40
+ filterset_fields = ['user']
41
+ search_fields = ['user__username', 'user__email']
42
+ ordering_fields = ['balance_usd', 'created_at', 'updated_at']
43
+
44
+ def get_queryset(self):
45
+ """Filter by user permissions and optimize queryset."""
46
+ queryset = super().get_queryset().select_related('user')
47
+
48
+ # Non-staff users can only see their own balance
49
+ if not self.request.user.is_staff:
50
+ queryset = queryset.filter(user=self.request.user)
51
+
52
+ return queryset
53
+
54
+ @action(detail=True, methods=['post'])
55
+ def add_funds(self, request, pk=None):
56
+ """
57
+ Add funds to user balance.
58
+
59
+ POST /api/balances/{id}/add_funds/
60
+ """
61
+ balance = self.get_object()
62
+
63
+ # Permission check: users can only add funds to their own balance
64
+ if not request.user.is_staff and balance.user != request.user:
65
+ return Response(
66
+ {'error': 'You can only add funds to your own balance'},
67
+ status=status.HTTP_403_FORBIDDEN
68
+ )
69
+
70
+ serializer = BalanceUpdateSerializer(
71
+ data=request.data,
72
+ context={
73
+ **self.get_serializer_context(),
74
+ 'user_pk': balance.user.id
75
+ }
76
+ )
77
+
78
+ if serializer.is_valid():
79
+ result = serializer.save()
80
+ return Response(result)
81
+
82
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
83
+
84
+ @action(detail=True, methods=['post'])
85
+ def withdraw_funds(self, request, pk=None):
86
+ """
87
+ Withdraw funds from user balance.
88
+
89
+ POST /api/balances/{id}/withdraw_funds/
90
+ """
91
+ balance = self.get_object()
92
+
93
+ # Permission check
94
+ if not request.user.is_staff and balance.user != request.user:
95
+ return Response(
96
+ {'error': 'You can only withdraw from your own balance'},
97
+ status=status.HTTP_403_FORBIDDEN
98
+ )
99
+
100
+ # Convert to negative amount for withdrawal
101
+ data = request.data.copy()
102
+ if 'amount' in data and data['amount'] > 0:
103
+ data['amount'] = -abs(data['amount'])
104
+
105
+ serializer = BalanceUpdateSerializer(
106
+ data=data,
107
+ context={
108
+ **self.get_serializer_context(),
109
+ 'user_pk': balance.user.id
110
+ }
111
+ )
112
+
113
+ if serializer.is_valid():
114
+ result = serializer.save()
115
+ return Response(result)
116
+
117
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
118
+
119
+ @action(detail=True, methods=['post'])
120
+ def transfer_funds(self, request, pk=None):
121
+ """
122
+ Transfer funds to another user.
123
+
124
+ POST /api/balances/{id}/transfer_funds/
125
+ """
126
+ balance = self.get_object()
127
+
128
+ # Permission check
129
+ if not request.user.is_staff and balance.user != request.user:
130
+ return Response(
131
+ {'error': 'You can only transfer from your own balance'},
132
+ status=status.HTTP_403_FORBIDDEN
133
+ )
134
+
135
+ serializer = FundsTransferSerializer(
136
+ data=request.data,
137
+ context=self.get_serializer_context()
138
+ )
139
+
140
+ if serializer.is_valid():
141
+ result = serializer.save()
142
+ return Response(result)
143
+
144
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
145
+
146
+ @action(detail=False, methods=['get'])
147
+ def analytics(self, request):
148
+ """
149
+ Get balance analytics.
150
+
151
+ GET /api/balances/analytics/?days=30
152
+ """
153
+ serializer = BalanceStatsSerializer(data=request.query_params)
154
+
155
+ if serializer.is_valid():
156
+ result = serializer.save()
157
+ return Response(result)
158
+
159
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
160
+
161
+ @action(detail=False, methods=['get'])
162
+ def summary(self, request):
163
+ """
164
+ Get balance summary for all users.
165
+
166
+ GET /api/balances/summary/
167
+ """
168
+ try:
169
+ queryset = self.filter_queryset(self.get_queryset())
170
+
171
+ summary = queryset.aggregate(
172
+ total_users=models.Count('id'),
173
+ total_balance=models.Sum('balance_usd'),
174
+ average_balance=models.Avg('balance_usd'),
175
+ users_with_balance=models.Count(
176
+ 'id',
177
+ filter=models.Q(balance_usd__gt=0)
178
+ ),
179
+ empty_balances=models.Count(
180
+ 'id',
181
+ filter=models.Q(balance_usd=0)
182
+ ),
183
+ )
184
+
185
+ return Response({
186
+ 'summary': {
187
+ **summary,
188
+ 'total_balance': float(summary['total_balance'] or 0),
189
+ 'average_balance': float(summary['average_balance'] or 0),
190
+ },
191
+ 'generated_at': timezone.now().isoformat()
192
+ })
193
+
194
+ except Exception as e:
195
+ logger.error(f"Balance summary failed: {e}")
196
+ return Response(
197
+ {'error': f'Summary generation failed: {e}'},
198
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
199
+ )
200
+
201
+
202
+ class TransactionViewSet(ReadOnlyPaymentViewSet):
203
+ """
204
+ Transaction ViewSet: /api/transactions/
205
+
206
+ Read-only access to transaction history with filtering.
207
+ """
208
+
209
+ queryset = Transaction.objects.all()
210
+ serializer_class = TransactionSerializer
211
+ permission_classes = [permissions.IsAuthenticated]
212
+ filterset_fields = ['user', 'transaction_type', 'payment_id']
213
+ search_fields = ['description', 'payment_id']
214
+ ordering_fields = ['created_at', 'amount']
215
+
216
+ def get_queryset(self):
217
+ """Filter by user permissions and optimize queryset."""
218
+ queryset = super().get_queryset().select_related('user')
219
+
220
+ # Non-staff users can only see their own transactions
221
+ if not self.request.user.is_staff:
222
+ queryset = queryset.filter(user=self.request.user)
223
+
224
+ return queryset
225
+
226
+ @action(detail=False, methods=['get'])
227
+ def by_type(self, request):
228
+ """
229
+ Get transactions grouped by type.
230
+
231
+ GET /api/transactions/by_type/
232
+ """
233
+ try:
234
+ queryset = self.filter_queryset(self.get_queryset())
235
+
236
+ type_stats = {}
237
+ for type_choice in Transaction.TransactionType.choices:
238
+ type_code = type_choice[0]
239
+ type_name = type_choice[1]
240
+
241
+ type_transactions = queryset.filter(transaction_type=type_code)
242
+
243
+ type_stats[type_code] = {
244
+ 'name': type_name,
245
+ 'total_transactions': type_transactions.count(),
246
+ 'total_amount': float(
247
+ type_transactions.aggregate(
248
+ total=models.Sum('amount')
249
+ )['total'] or 0
250
+ ),
251
+ 'average_amount': float(
252
+ type_transactions.aggregate(
253
+ avg=models.Avg('amount')
254
+ )['avg'] or 0
255
+ ),
256
+ }
257
+
258
+ return Response({
259
+ 'type_stats': type_stats,
260
+ 'generated_at': timezone.now().isoformat()
261
+ })
262
+
263
+ except Exception as e:
264
+ logger.error(f"Transaction type stats failed: {e}")
265
+ return Response(
266
+ {'error': f'Type stats failed: {e}'},
267
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
268
+ )
269
+
270
+ @action(detail=False, methods=['get'])
271
+ def recent(self, request):
272
+ """
273
+ Get recent transactions.
274
+
275
+ GET /api/transactions/recent/?limit=10
276
+ """
277
+ try:
278
+ limit = int(request.query_params.get('limit', 10))
279
+ limit = min(limit, 100) # Cap at 100
280
+
281
+ queryset = self.filter_queryset(self.get_queryset())
282
+ recent_transactions = queryset.order_by('-created_at')[:limit]
283
+
284
+ serializer = self.get_serializer(recent_transactions, many=True)
285
+
286
+ return Response({
287
+ 'transactions': serializer.data,
288
+ 'count': len(serializer.data),
289
+ 'limit': limit,
290
+ 'generated_at': timezone.now().isoformat()
291
+ })
292
+
293
+ except Exception as e:
294
+ logger.error(f"Recent transactions failed: {e}")
295
+ return Response(
296
+ {'error': f'Recent transactions failed: {e}'},
297
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
298
+ )
299
+
300
+
301
+ class UserTransactionViewSet(NestedPaymentViewSet):
302
+ """
303
+ User-specific transaction ViewSet: /api/users/{user_id}/transactions/
304
+
305
+ User-scoped access to transaction history.
306
+ """
307
+
308
+ queryset = Transaction.objects.all()
309
+ serializer_class = TransactionSerializer
310
+ permission_classes = [permissions.IsAuthenticated]
311
+ filterset_fields = ['transaction_type', 'payment_id']
312
+ search_fields = ['description', 'payment_id']
313
+ ordering_fields = ['created_at', 'amount']
314
+
315
+ # Nested ViewSet configuration
316
+ parent_lookup_field = 'user_pk'
317
+ parent_model_field = 'user'
318
+
319
+ # Read-only operations only
320
+ http_method_names = ['get', 'head', 'options']
321
+
322
+ def get_queryset(self):
323
+ """Filter by user and optimize queryset."""
324
+ queryset = super().get_queryset()
325
+
326
+ # Additional permission check: users can only see their own transactions
327
+ if not self.request.user.is_staff:
328
+ user_id = self.kwargs.get('user_pk')
329
+ if str(self.request.user.id) != str(user_id):
330
+ return queryset.none()
331
+
332
+ return queryset
333
+
334
+ @action(detail=False, methods=['get'])
335
+ def summary(self, request, user_pk=None):
336
+ """
337
+ Get user transaction summary.
338
+
339
+ GET /api/users/{user_id}/transactions/summary/
340
+ """
341
+ try:
342
+ queryset = self.filter_queryset(self.get_queryset())
343
+
344
+ summary = queryset.aggregate(
345
+ total_transactions=models.Count('id'),
346
+ total_credits=models.Sum(
347
+ 'amount',
348
+ filter=models.Q(amount__gt=0)
349
+ ),
350
+ total_debits=models.Sum(
351
+ 'amount',
352
+ filter=models.Q(amount__lt=0)
353
+ ),
354
+ net_amount=models.Sum('amount'),
355
+ )
356
+
357
+ # Get type breakdown
358
+ type_breakdown = dict(
359
+ queryset.values('transaction_type')
360
+ .annotate(count=models.Count('id'))
361
+ .values_list('transaction_type', 'count')
362
+ )
363
+
364
+ return Response({
365
+ 'user_id': user_pk,
366
+ 'summary': {
367
+ **summary,
368
+ 'total_credits': float(summary['total_credits'] or 0),
369
+ 'total_debits': float(abs(summary['total_debits'] or 0)),
370
+ 'net_amount': float(summary['net_amount'] or 0),
371
+ 'type_breakdown': type_breakdown,
372
+ },
373
+ 'generated_at': timezone.now().isoformat()
374
+ })
375
+
376
+ except Exception as e:
377
+ logger.error(f"User transaction summary failed: {e}")
378
+ return Response(
379
+ {'error': f'Summary generation failed: {e}'},
380
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
381
+ )
@@ -0,0 +1,298 @@
1
+ """
2
+ Base ViewSet classes for the Universal Payment System v2.0.
3
+
4
+ Common functionality for all payment system ViewSets.
5
+ """
6
+
7
+ from rest_framework import viewsets, permissions, status
8
+ from rest_framework.decorators import action
9
+ from rest_framework.response import Response
10
+ from django_filters.rest_framework import DjangoFilterBackend
11
+ from rest_framework.filters import SearchFilter, OrderingFilter
12
+ from rest_framework.exceptions import NotFound
13
+
14
+ from django.db.models import Count, Sum, Avg, Q
15
+ from django.utils import timezone
16
+ from datetime import timedelta
17
+ from typing import Dict, Any
18
+
19
+ from django_cfg.modules.django_logger import get_logger
20
+
21
+ logger = get_logger("api_viewsets")
22
+
23
+
24
+ class PaymentBaseViewSet(viewsets.ModelViewSet):
25
+ """
26
+ Enhanced base ViewSet with common functionality.
27
+
28
+ Provides standard CRUD operations plus common actions like stats,
29
+ health checks, and optimized querysets.
30
+ """
31
+
32
+ permission_classes = [permissions.IsAuthenticated]
33
+ filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
34
+ ordering = ['-created_at']
35
+
36
+ # Serializer classes mapping for different actions
37
+ serializer_classes = {}
38
+
39
+ def get_queryset(self):
40
+ """
41
+ Optimized queryset with select_related and prefetch_related.
42
+
43
+ Override in subclasses to add specific optimizations.
44
+ """
45
+ queryset = super().get_queryset()
46
+
47
+ # Add common optimizations
48
+ if hasattr(self.queryset.model, 'user'):
49
+ queryset = queryset.select_related('user')
50
+
51
+ return queryset
52
+
53
+ def get_serializer_class(self):
54
+ """
55
+ Dynamic serializer selection based on action.
56
+
57
+ Uses serializer_classes mapping or falls back to default.
58
+ """
59
+ serializer_classes = getattr(self, 'serializer_classes', {})
60
+ return serializer_classes.get(self.action, self.serializer_class)
61
+
62
+ def get_serializer_context(self):
63
+ """
64
+ Enhanced serializer context with additional data.
65
+ """
66
+ context = super().get_serializer_context()
67
+ context.update({
68
+ 'action': self.action,
69
+ 'user': self.request.user,
70
+ })
71
+
72
+ # Add object ID for detail actions
73
+ if self.action in ['retrieve', 'update', 'partial_update', 'destroy']:
74
+ context['object_id'] = self.kwargs.get('pk')
75
+
76
+ # Add parent object ID for nested routes
77
+ for key, value in self.kwargs.items():
78
+ if key.endswith('_pk'):
79
+ context[key] = value
80
+
81
+ return context
82
+
83
+ @action(detail=False, methods=['get'])
84
+ def stats(self, request):
85
+ """
86
+ Get statistics for the current queryset.
87
+
88
+ Returns counts, aggregates, and breakdowns.
89
+ """
90
+ try:
91
+ queryset = self.filter_queryset(self.get_queryset())
92
+
93
+ # Basic counts
94
+ total_count = queryset.count()
95
+
96
+ stats = {
97
+ 'total_count': total_count,
98
+ 'generated_at': timezone.now().isoformat(),
99
+ }
100
+
101
+ # Add status breakdown if model has status field
102
+ if hasattr(queryset.model, 'status'):
103
+ stats['status_breakdown'] = self._get_status_breakdown(queryset)
104
+
105
+ # Add amount summary if model has amount fields
106
+ if hasattr(queryset.model, 'amount_usd'):
107
+ stats['amount_summary'] = self._get_amount_summary(queryset)
108
+
109
+ # Add time-based breakdown
110
+ stats['time_breakdown'] = self._get_time_breakdown(queryset)
111
+
112
+ return Response(stats)
113
+
114
+ except Exception as e:
115
+ logger.error(f"Stats generation failed: {e}")
116
+ return Response(
117
+ {'error': f'Stats generation failed: {e}'},
118
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
119
+ )
120
+
121
+ @action(detail=False, methods=['get'])
122
+ def health(self, request):
123
+ """
124
+ Health check for the ViewSet and related services.
125
+
126
+ Returns service status and basic metrics.
127
+ """
128
+ try:
129
+ queryset = self.get_queryset()
130
+
131
+ # Basic health metrics
132
+ health_data = {
133
+ 'service': self.__class__.__name__,
134
+ 'status': 'healthy',
135
+ 'total_records': queryset.count(),
136
+ 'model': queryset.model.__name__,
137
+ 'timestamp': timezone.now().isoformat(),
138
+ }
139
+
140
+ # Check recent activity (last 24 hours)
141
+ if hasattr(queryset.model, 'created_at'):
142
+ recent_count = queryset.filter(
143
+ created_at__gte=timezone.now() - timedelta(hours=24)
144
+ ).count()
145
+ health_data['recent_activity'] = recent_count
146
+
147
+ return Response(health_data)
148
+
149
+ except Exception as e:
150
+ logger.error(f"Health check failed: {e}")
151
+ return Response(
152
+ {
153
+ 'service': self.__class__.__name__,
154
+ 'status': 'unhealthy',
155
+ 'error': str(e),
156
+ 'timestamp': timezone.now().isoformat(),
157
+ },
158
+ status=status.HTTP_503_SERVICE_UNAVAILABLE
159
+ )
160
+
161
+ def _get_status_breakdown(self, queryset) -> Dict[str, int]:
162
+ """Get status breakdown for statistics."""
163
+ return dict(
164
+ queryset.values('status')
165
+ .annotate(count=Count('id'))
166
+ .values_list('status', 'count')
167
+ )
168
+
169
+ def _get_amount_summary(self, queryset) -> Dict[str, Any]:
170
+ """Get amount summary for statistics."""
171
+ aggregates = queryset.aggregate(
172
+ total_amount=Sum('amount_usd'),
173
+ average_amount=Avg('amount_usd'),
174
+ count=Count('id')
175
+ )
176
+
177
+ return {
178
+ 'total_amount_usd': float(aggregates['total_amount'] or 0),
179
+ 'average_amount_usd': float(aggregates['average_amount'] or 0),
180
+ 'transaction_count': aggregates['count'],
181
+ }
182
+
183
+ def _get_time_breakdown(self, queryset) -> Dict[str, int]:
184
+ """Get time-based breakdown for statistics."""
185
+ if not hasattr(queryset.model, 'created_at'):
186
+ return {}
187
+
188
+ now = timezone.now()
189
+
190
+ return {
191
+ 'last_24h': queryset.filter(
192
+ created_at__gte=now - timedelta(hours=24)
193
+ ).count(),
194
+ 'last_7d': queryset.filter(
195
+ created_at__gte=now - timedelta(days=7)
196
+ ).count(),
197
+ 'last_30d': queryset.filter(
198
+ created_at__gte=now - timedelta(days=30)
199
+ ).count(),
200
+ }
201
+
202
+ def handle_exception(self, exc):
203
+ """
204
+ Enhanced exception handling with logging.
205
+ """
206
+ logger.error(f"ViewSet exception in {self.__class__.__name__}: {exc}", extra={
207
+ 'action': getattr(self, 'action', 'unknown'),
208
+ 'user_id': getattr(self.request.user, 'id', None) if hasattr(self, 'request') else None,
209
+ 'exception_type': type(exc).__name__,
210
+ })
211
+
212
+ return super().handle_exception(exc)
213
+
214
+
215
+ class ReadOnlyPaymentViewSet(PaymentBaseViewSet):
216
+ """
217
+ Read-only base ViewSet for resources that shouldn't be modified via API.
218
+
219
+ Provides list, retrieve, and stats actions only.
220
+ """
221
+
222
+ http_method_names = ['get', 'head', 'options']
223
+
224
+ def create(self, request, *args, **kwargs):
225
+ """Disable create action."""
226
+ return Response(
227
+ {'error': 'Create operation not allowed'},
228
+ status=status.HTTP_405_METHOD_NOT_ALLOWED
229
+ )
230
+
231
+ def update(self, request, *args, **kwargs):
232
+ """Disable update action."""
233
+ return Response(
234
+ {'error': 'Update operation not allowed'},
235
+ status=status.HTTP_405_METHOD_NOT_ALLOWED
236
+ )
237
+
238
+ def partial_update(self, request, *args, **kwargs):
239
+ """Disable partial update action."""
240
+ return Response(
241
+ {'error': 'Update operation not allowed'},
242
+ status=status.HTTP_405_METHOD_NOT_ALLOWED
243
+ )
244
+
245
+ def destroy(self, request, *args, **kwargs):
246
+ """Disable destroy action."""
247
+ return Response(
248
+ {'error': 'Delete operation not allowed'},
249
+ status=status.HTTP_405_METHOD_NOT_ALLOWED
250
+ )
251
+
252
+
253
+ class NestedPaymentViewSet(PaymentBaseViewSet):
254
+ """
255
+ Base ViewSet for nested resources (e.g., /users/{id}/payments/).
256
+
257
+ Automatically filters queryset by parent object and sets parent on creation.
258
+ """
259
+
260
+ parent_lookup_field = 'user_pk' # Override in subclasses
261
+ parent_model_field = 'user' # Override in subclasses
262
+
263
+ def get_queryset(self):
264
+ """Filter queryset by parent object from URL."""
265
+ queryset = super().get_queryset()
266
+
267
+ parent_id = self.kwargs.get(self.parent_lookup_field)
268
+ if parent_id:
269
+ filter_kwargs = {self.parent_model_field + '_id': parent_id}
270
+ queryset = queryset.filter(**filter_kwargs)
271
+
272
+ return queryset
273
+
274
+ def perform_create(self, serializer):
275
+ """Set parent object when creating nested resource."""
276
+ parent_id = self.kwargs.get(self.parent_lookup_field)
277
+ if parent_id:
278
+ # Get parent model class
279
+ parent_field = getattr(self.queryset.model, self.parent_model_field)
280
+ parent_model = parent_field.field.related_model
281
+
282
+ try:
283
+ parent_obj = parent_model.objects.get(id=parent_id)
284
+ serializer.save(**{self.parent_model_field: parent_obj})
285
+ except parent_model.DoesNotExist:
286
+ raise NotFound(f"Parent object not found: {parent_id}")
287
+ else:
288
+ serializer.save()
289
+
290
+ def get_serializer_context(self):
291
+ """Add parent object ID to serializer context."""
292
+ context = super().get_serializer_context()
293
+
294
+ parent_id = self.kwargs.get(self.parent_lookup_field)
295
+ if parent_id:
296
+ context[self.parent_lookup_field] = parent_id
297
+
298
+ return context