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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -10
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +526 -222
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +465 -70
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
- django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
- django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +13 -18
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +172 -148
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -285
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +346 -467
- django_cfg/apps/payments/services/core/subscription_service.py +425 -481
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +234 -174
- django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
- django_cfg/apps/payments/services/providers/registry.py +367 -301
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +210 -129
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -103
- django_cfg/apps/payments/signals/subscription_signals.py +194 -142
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +45 -48
- django_cfg/apps/payments/urls_admin.py +33 -42
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +40 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -492
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +64 -16
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -146
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
- django_cfg/apps/payments/managers/__init__.py +0 -23
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -306
- django_cfg/apps/payments/managers/payment_manager.py +0 -192
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -57
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -63
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -461
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
- django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
- django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
- django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
- django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
- django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -635
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
- django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
- django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
- django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
- django_cfg/apps/payments/templates/payments/stats.html +0 -261
- django_cfg/apps/payments/templates/payments/test.html +0 -213
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/utils/__init__.py +0 -43
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -239
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -63
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -122
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -451
- django_cfg/apps/payments/views/templates/base.py +0 -212
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -158
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -244
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -66
- django_cfg/core/integration.py +0 -160
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {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
|