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
@@ -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