django-cfg 1.2.29__py3-none-any.whl → 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/health/views.py +4 -2
  3. django_cfg/apps/knowbase/config/settings.py +16 -15
  4. django_cfg/apps/payments/README.md +326 -0
  5. django_cfg/apps/payments/admin/__init__.py +20 -9
  6. django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
  7. django_cfg/apps/payments/admin/balance_admin.py +592 -297
  8. django_cfg/apps/payments/admin/currencies_admin.py +600 -108
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +470 -64
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
  39. django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
  43. django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +16 -20
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +207 -67
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -284
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +344 -468
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -484
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +232 -71
  74. django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
  75. django_cfg/apps/payments/services/providers/registry.py +429 -80
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +211 -130
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +129 -98
  85. django_cfg/apps/payments/signals/subscription_signals.py +195 -143
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +46 -47
  93. django_cfg/apps/payments/urls_admin.py +49 -0
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/apps/tasks/urls.py +0 -2
  110. django_cfg/apps/tasks/urls_admin.py +14 -0
  111. django_cfg/apps/urls.py +4 -4
  112. django_cfg/config.py +1 -1
  113. django_cfg/core/config.py +75 -4
  114. django_cfg/core/generation.py +25 -4
  115. django_cfg/core/integration/README.md +363 -0
  116. django_cfg/core/integration/__init__.py +47 -0
  117. django_cfg/core/integration/commands_collector.py +239 -0
  118. django_cfg/core/integration/display/__init__.py +15 -0
  119. django_cfg/core/integration/display/base.py +157 -0
  120. django_cfg/core/integration/display/ngrok.py +164 -0
  121. django_cfg/core/integration/display/startup.py +815 -0
  122. django_cfg/core/integration/url_integration.py +123 -0
  123. django_cfg/core/integration/version_checker.py +160 -0
  124. django_cfg/management/commands/auto_generate.py +4 -0
  125. django_cfg/management/commands/check_settings.py +6 -0
  126. django_cfg/management/commands/clear_constance.py +5 -2
  127. django_cfg/management/commands/create_token.py +6 -0
  128. django_cfg/management/commands/list_urls.py +6 -0
  129. django_cfg/management/commands/migrate_all.py +6 -0
  130. django_cfg/management/commands/migrator.py +3 -0
  131. django_cfg/management/commands/rundramatiq.py +6 -0
  132. django_cfg/management/commands/runserver_ngrok.py +51 -29
  133. django_cfg/management/commands/script.py +6 -0
  134. django_cfg/management/commands/show_config.py +12 -2
  135. django_cfg/management/commands/show_urls.py +4 -0
  136. django_cfg/management/commands/superuser.py +6 -0
  137. django_cfg/management/commands/task_clear.py +4 -1
  138. django_cfg/management/commands/task_status.py +3 -1
  139. django_cfg/management/commands/test_email.py +3 -0
  140. django_cfg/management/commands/test_telegram.py +6 -0
  141. django_cfg/management/commands/test_twilio.py +6 -0
  142. django_cfg/management/commands/tree.py +6 -0
  143. django_cfg/management/commands/validate_config.py +155 -149
  144. django_cfg/models/constance.py +31 -11
  145. django_cfg/models/payments.py +175 -498
  146. django_cfg/modules/django_currency/__init__.py +16 -11
  147. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  148. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  149. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  150. django_cfg/modules/django_currency/core/__init__.py +1 -7
  151. django_cfg/modules/django_currency/core/converter.py +18 -23
  152. django_cfg/modules/django_currency/core/models.py +122 -11
  153. django_cfg/modules/django_currency/database/__init__.py +4 -4
  154. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  155. django_cfg/modules/django_logger.py +160 -146
  156. django_cfg/modules/django_unfold/dashboard.py +65 -12
  157. django_cfg/registry/core.py +1 -0
  158. django_cfg/template_archive/django_sample.zip +0 -0
  159. django_cfg/templates/admin/components/action_grid.html +9 -9
  160. django_cfg/templates/admin/components/metric_card.html +5 -5
  161. django_cfg/templates/admin/components/status_badge.html +2 -2
  162. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  163. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  164. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  165. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  166. django_cfg/utils/smart_defaults.py +222 -571
  167. django_cfg/utils/toolkit.py +51 -11
  168. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
  169. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
  170. django_cfg/apps/payments/__init__.py +0 -8
  171. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  172. django_cfg/apps/payments/config/module.py +0 -70
  173. django_cfg/apps/payments/config/providers.py +0 -105
  174. django_cfg/apps/payments/config/settings.py +0 -96
  175. django_cfg/apps/payments/config/utils.py +0 -52
  176. django_cfg/apps/payments/decorators.py +0 -291
  177. django_cfg/apps/payments/management/commands/README.md +0 -178
  178. django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
  179. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  180. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  181. django_cfg/apps/payments/managers/__init__.py +0 -22
  182. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  183. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  184. django_cfg/apps/payments/managers/currency_manager.py +0 -83
  185. django_cfg/apps/payments/managers/payment_manager.py +0 -44
  186. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  187. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  188. django_cfg/apps/payments/models/events.py +0 -73
  189. django_cfg/apps/payments/serializers/__init__.py +0 -56
  190. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  191. django_cfg/apps/payments/serializers/balance.py +0 -59
  192. django_cfg/apps/payments/serializers/currencies.py +0 -55
  193. django_cfg/apps/payments/serializers/payments.py +0 -62
  194. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  195. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  196. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  197. django_cfg/apps/payments/services/cache/base.py +0 -30
  198. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  199. django_cfg/apps/payments/services/internal_types.py +0 -297
  200. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  201. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  202. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
  203. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  204. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  205. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  206. django_cfg/apps/payments/services/security/__init__.py +0 -34
  207. django_cfg/apps/payments/services/security/error_handler.py +0 -637
  208. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  209. django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
  210. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  211. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  212. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  213. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  214. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  215. django_cfg/apps/payments/tasks/__init__.py +0 -12
  216. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  217. django_cfg/apps/payments/templates/payments/base.html +0 -182
  218. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  219. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  220. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
  221. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  222. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
  223. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
  224. django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
  225. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  226. django_cfg/apps/payments/urls_templates.py +0 -52
  227. django_cfg/apps/payments/utils/__init__.py +0 -45
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -245
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -62
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -111
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -312
  241. django_cfg/apps/payments/views/templates/base.py +0 -204
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -164
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -240
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -65
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  252. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  253. django_cfg/template_archive/.gitignore +0 -1
  254. django_cfg/template_archive/__init__.py +0 -0
  255. django_cfg/urls.py +0 -33
  256. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  257. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  258. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -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