django-cfg 1.3.3__py3-none-any.whl → 1.3.5__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 (101) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin_interface/old/payments/base.html +175 -0
  3. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +125 -0
  4. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +113 -0
  5. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +35 -0
  6. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +309 -0
  7. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +303 -0
  8. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +382 -0
  9. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +518 -0
  10. django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/components.css +248 -9
  11. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +163 -0
  12. django_cfg/apps/payments/admin_interface/serializers/__init__.py +39 -0
  13. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +149 -0
  14. django_cfg/apps/payments/admin_interface/serializers/webhook_serializers.py +114 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/base.html +55 -90
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/dialog.html +81 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_help_dialog.html +112 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_status.html +175 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +21 -17
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +123 -250
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +170 -269
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +152 -355
  23. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +202 -551
  24. django_cfg/apps/payments/admin_interface/views/__init__.py +25 -14
  25. django_cfg/apps/payments/admin_interface/views/api/__init__.py +20 -0
  26. django_cfg/apps/payments/admin_interface/views/api/payments.py +191 -0
  27. django_cfg/apps/payments/admin_interface/views/api/stats.py +206 -0
  28. django_cfg/apps/payments/admin_interface/views/api/users.py +60 -0
  29. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +257 -0
  30. django_cfg/apps/payments/admin_interface/views/api/webhook_public.py +70 -0
  31. django_cfg/apps/payments/admin_interface/views/base.py +114 -0
  32. django_cfg/apps/payments/admin_interface/views/dashboard.py +60 -0
  33. django_cfg/apps/payments/admin_interface/views/forms.py +94 -0
  34. django_cfg/apps/payments/config/helpers.py +2 -2
  35. django_cfg/apps/payments/management/commands/cleanup_expired_data.py +16 -6
  36. django_cfg/apps/payments/management/commands/currency_stats.py +72 -5
  37. django_cfg/apps/payments/management/commands/manage_currencies.py +9 -20
  38. django_cfg/apps/payments/management/commands/manage_providers.py +5 -5
  39. django_cfg/apps/payments/middleware/api_access.py +35 -34
  40. django_cfg/apps/payments/migrations/0001_initial.py +1 -1
  41. django_cfg/apps/payments/models/managers/api_key_managers.py +4 -0
  42. django_cfg/apps/payments/models/managers/payment_managers.py +5 -0
  43. django_cfg/apps/payments/models/subscriptions.py +0 -24
  44. django_cfg/apps/payments/services/cache/__init__.py +1 -1
  45. django_cfg/apps/payments/services/core/balance_service.py +13 -2
  46. django_cfg/apps/payments/services/integrations/ngrok_service.py +3 -3
  47. django_cfg/apps/payments/services/providers/registry.py +20 -0
  48. django_cfg/apps/payments/signals/balance_signals.py +7 -4
  49. django_cfg/apps/payments/static/payments/js/api-client.js +385 -0
  50. django_cfg/apps/payments/static/payments/js/ngrok-status.js +58 -0
  51. django_cfg/apps/payments/static/payments/js/payment-dashboard.js +50 -0
  52. django_cfg/apps/payments/static/payments/js/payment-form.js +175 -0
  53. django_cfg/apps/payments/static/payments/js/payment-list.js +95 -0
  54. django_cfg/apps/payments/static/payments/js/webhook-dashboard.js +154 -0
  55. django_cfg/apps/payments/urls.py +4 -0
  56. django_cfg/apps/payments/urls_admin.py +37 -18
  57. django_cfg/apps/payments/views/api/api_keys.py +14 -0
  58. django_cfg/apps/payments/views/api/base.py +1 -0
  59. django_cfg/apps/payments/views/api/currencies.py +2 -2
  60. django_cfg/apps/payments/views/api/payments.py +11 -5
  61. django_cfg/apps/payments/views/api/subscriptions.py +36 -31
  62. django_cfg/apps/payments/views/overview/__init__.py +40 -0
  63. django_cfg/apps/payments/views/overview/serializers.py +205 -0
  64. django_cfg/apps/payments/views/overview/services.py +439 -0
  65. django_cfg/apps/payments/views/overview/urls.py +27 -0
  66. django_cfg/apps/payments/views/overview/views.py +231 -0
  67. django_cfg/apps/payments/views/serializers/api_keys.py +20 -6
  68. django_cfg/apps/payments/views/serializers/balances.py +5 -8
  69. django_cfg/apps/payments/views/serializers/currencies.py +2 -6
  70. django_cfg/apps/payments/views/serializers/payments.py +37 -32
  71. django_cfg/apps/payments/views/serializers/subscriptions.py +4 -26
  72. django_cfg/apps/urls.py +2 -1
  73. django_cfg/core/config.py +25 -15
  74. django_cfg/core/generation.py +12 -12
  75. django_cfg/core/integration/display/startup.py +1 -1
  76. django_cfg/core/validation.py +4 -4
  77. django_cfg/management/commands/show_config.py +2 -2
  78. django_cfg/management/commands/tree.py +1 -3
  79. django_cfg/middleware/__init__.py +2 -0
  80. django_cfg/middleware/static_nocache.py +55 -0
  81. django_cfg/models/payments.py +13 -15
  82. django_cfg/models/security.py +15 -0
  83. django_cfg/modules/django_ngrok.py +6 -0
  84. django_cfg/modules/django_unfold/dashboard.py +1 -3
  85. django_cfg/utils/smart_defaults.py +41 -1
  86. {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/METADATA +1 -1
  87. {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/RECORD +98 -65
  88. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +0 -38
  89. django_cfg/apps/payments/admin_interface/views/payment_views.py +0 -259
  90. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +0 -37
  91. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/loading_spinner.html +0 -0
  92. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/notification.html +0 -0
  93. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/provider_card.html +0 -0
  94. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/currency_converter.html +0 -0
  95. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/payment_status.html +0 -0
  96. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/dashboard.css +0 -0
  97. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/components.js +0 -0
  98. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/utils.js +0 -0
  99. {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/WHEEL +0 -0
  100. {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/entry_points.txt +0 -0
  101. {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Payment List Component
3
+ * Handles payment listing, filtering, and pagination
4
+ */
5
+ function paymentList() {
6
+ return {
7
+ loading: false,
8
+ payments: [],
9
+ filters: {
10
+ search: '',
11
+ status: ''
12
+ },
13
+ currentPage: 1,
14
+ pageSize: 10,
15
+
16
+ get filteredPayments() {
17
+ let filtered = this.payments;
18
+
19
+ if (this.filters.search) {
20
+ const search = this.filters.search.toLowerCase();
21
+ filtered = filtered.filter(payment =>
22
+ payment.id.toLowerCase().includes(search) ||
23
+ payment.external_id?.toLowerCase().includes(search) ||
24
+ payment.provider.toLowerCase().includes(search)
25
+ );
26
+ }
27
+
28
+ if (this.filters.status) {
29
+ filtered = filtered.filter(payment => payment.status === this.filters.status);
30
+ }
31
+
32
+ return filtered;
33
+ },
34
+
35
+ get paginatedPayments() {
36
+ const start = (this.currentPage - 1) * this.pageSize;
37
+ const end = start + this.pageSize;
38
+ return this.filteredPayments.slice(start, end);
39
+ },
40
+
41
+ async init() {
42
+ await this.loadPayments();
43
+ // Auto-refresh every 30 seconds
44
+ setInterval(() => this.loadPayments(), 30000);
45
+ },
46
+
47
+ async loadPayments() {
48
+ this.loading = true;
49
+ try {
50
+ const response = await PaymentAPI.admin.payments.list(this.filters);
51
+ this.payments = response.payments || response.results || [];
52
+ this.pagination = {
53
+ total: response.total || response.count || 0,
54
+ page: response.page || 1,
55
+ per_page: response.per_page || 50,
56
+ has_next: response.has_next || false,
57
+ has_previous: response.has_previous || false
58
+ };
59
+ } catch (error) {
60
+ console.error('Failed to load payments:', error);
61
+ this.payments = [];
62
+ PaymentAPI.utils.showNotification('Failed to load payments', 'error');
63
+ } finally {
64
+ this.loading = false;
65
+ }
66
+ },
67
+
68
+ async refreshPayments() {
69
+ await this.loadPayments();
70
+ PaymentAPI.utils.showNotification('Payments refreshed', 'success');
71
+ },
72
+
73
+ async refreshPayment(paymentId) {
74
+ try {
75
+ const updatedPayment = await PaymentAPI.payments.get(paymentId);
76
+ const index = this.payments.findIndex(p => p.id === paymentId);
77
+ if (index !== -1) {
78
+ this.payments[index] = updatedPayment;
79
+ }
80
+ PaymentAPI.utils.showNotification('Payment updated', 'success');
81
+ } catch (error) {
82
+ console.error('Failed to refresh payment:', error);
83
+ PaymentAPI.utils.showNotification('Failed to refresh payment', 'error');
84
+ }
85
+ },
86
+
87
+ applyFilters() {
88
+ this.currentPage = 1;
89
+ },
90
+
91
+ formatDate(dateString) {
92
+ return PaymentAPI.utils.formatDate(dateString);
93
+ }
94
+ };
95
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Webhook Dashboard Component
3
+ * Handles webhook events monitoring and testing
4
+ */
5
+ function webhookDashboard() {
6
+ return {
7
+ loading: false,
8
+ testLoading: false,
9
+ events: [],
10
+ filters: {
11
+ event_type: '',
12
+ status: ''
13
+ },
14
+ stats: {
15
+ total: 0,
16
+ successful: 0,
17
+ failed: 0,
18
+ successRate: 0
19
+ },
20
+ testForm: {
21
+ url: '',
22
+ event_type: ''
23
+ },
24
+ showEventModal: false,
25
+ selectedEvent: null,
26
+
27
+ get filteredEvents() {
28
+ let filtered = this.events;
29
+
30
+ if (this.filters.event_type) {
31
+ filtered = filtered.filter(event => event.event_type === this.filters.event_type);
32
+ }
33
+
34
+ if (this.filters.status) {
35
+ filtered = filtered.filter(event => event.status === this.filters.status);
36
+ }
37
+
38
+ return filtered;
39
+ },
40
+
41
+ async init() {
42
+ await this.loadEvents();
43
+ await this.loadStats();
44
+ // Auto-refresh every 30 seconds
45
+ setInterval(() => this.refreshEvents(), 30000);
46
+ },
47
+
48
+ async loadEvents() {
49
+ this.loading = true;
50
+ try {
51
+ const response = await PaymentAPI.admin.webhooks.events.list();
52
+ this.events = response.events || [];
53
+ this.pagination = {
54
+ total: response.total || 0,
55
+ page: response.page || 1,
56
+ per_page: response.per_page || 50,
57
+ has_next: response.has_next || false,
58
+ has_previous: response.has_previous || false
59
+ };
60
+ } catch (error) {
61
+ console.error('Failed to load events:', error);
62
+ this.events = [];
63
+ PaymentAPI.utils.showNotification('Failed to load webhook events', 'error');
64
+ } finally {
65
+ this.loading = false;
66
+ }
67
+ },
68
+
69
+ async loadStats() {
70
+ try {
71
+ this.stats = await PaymentAPI.admin.webhooks.stats();
72
+ } catch (error) {
73
+ console.error('Failed to load stats:', error);
74
+ // Set default stats on error
75
+ this.stats = {
76
+ total: 0,
77
+ successful: 0,
78
+ failed: 0,
79
+ successRate: 0
80
+ };
81
+ PaymentAPI.utils.showNotification('Failed to load webhook stats', 'error');
82
+ }
83
+ },
84
+
85
+ async refreshEvents() {
86
+ await this.loadEvents();
87
+ await this.loadStats();
88
+ },
89
+
90
+ async clearEvents() {
91
+ if (confirm('Are you sure you want to clear all events?')) {
92
+ try {
93
+ await PaymentAPI.admin.webhooks.events.clearAll(1);
94
+ this.events = [];
95
+ await this.loadStats();
96
+ PaymentAPI.utils.showNotification('All events cleared', 'success');
97
+ } catch (error) {
98
+ console.error('Failed to clear events:', error);
99
+ PaymentAPI.utils.showNotification('Failed to clear events', 'error');
100
+ }
101
+ }
102
+ },
103
+
104
+ async testWebhook() {
105
+ this.testLoading = true;
106
+ try {
107
+ await PaymentAPI.admin.webhookTest.send(this.testForm.url, this.testForm.event_type);
108
+ PaymentAPI.utils.showNotification('Test webhook sent successfully!', 'success');
109
+ this.testForm = { url: '', event_type: '' };
110
+ await this.refreshEvents();
111
+ } catch (error) {
112
+ console.error('Failed to test webhook:', error);
113
+ PaymentAPI.utils.showNotification('Failed to send test webhook', 'error');
114
+ } finally {
115
+ this.testLoading = false;
116
+ }
117
+ },
118
+
119
+ async retryEvent(eventId) {
120
+ try {
121
+ await PaymentAPI.admin.webhooks.events.retry(1, eventId);
122
+ await this.refreshEvents();
123
+ PaymentAPI.utils.showNotification('Event retried', 'success');
124
+ } catch (error) {
125
+ console.error('Failed to retry event:', error);
126
+ PaymentAPI.utils.showNotification('Failed to retry event', 'error');
127
+ }
128
+ },
129
+
130
+ async retryFailedEvents() {
131
+ try {
132
+ await PaymentAPI.admin.webhooks.events.retryFailed(1);
133
+ await this.refreshEvents();
134
+ PaymentAPI.utils.showNotification('Failed events retried', 'success');
135
+ } catch (error) {
136
+ console.error('Failed to retry failed events:', error);
137
+ PaymentAPI.utils.showNotification('Failed to retry events', 'error');
138
+ }
139
+ },
140
+
141
+ viewEvent(event) {
142
+ this.selectedEvent = event;
143
+ this.showEventModal = true;
144
+ },
145
+
146
+ applyFilters() {
147
+ // Filters are applied automatically via computed property
148
+ },
149
+
150
+ formatDate(dateString) {
151
+ return PaymentAPI.utils.formatDate(dateString);
152
+ }
153
+ };
154
+ }
@@ -16,6 +16,7 @@ from .views.api import (
16
16
  APIKeyViewSet, UserAPIKeyViewSet, APIKeyCreateView, APIKeyValidateView,
17
17
  UniversalWebhookView, webhook_health_check, webhook_stats, supported_providers,
18
18
  )
19
+ from .views.overview import urls as overview_urls
19
20
 
20
21
  app_name = 'cfg_payments'
21
22
 
@@ -74,4 +75,7 @@ urlpatterns = [
74
75
 
75
76
  # Health check endpoint
76
77
  path('health/', PaymentViewSet.as_view({'get': 'health'}), name='health-check'),
78
+
79
+ # Overview dashboard endpoints
80
+ path('overview/', include(overview_urls)),
77
81
  ]
@@ -1,49 +1,68 @@
1
1
  """
2
2
  Admin URLs for Universal Payment System v2.0.
3
3
 
4
- Internal dashboard and management interfaces.
4
+ Internal dashboard and management interfaces with DRF nested routing.
5
5
  All URLs require staff/superuser access.
6
6
  """
7
7
 
8
8
  from django.urls import path, include
9
9
  from django.contrib.admin.views.decorators import staff_member_required
10
+ from rest_framework.routers import DefaultRouter
11
+ from rest_framework_nested import routers
10
12
 
11
13
  from .admin_interface.views import (
12
- WebhookDashboardView,
14
+ PaymentDashboardView,
13
15
  PaymentFormView,
14
- PaymentStatusView,
16
+ PaymentDetailView,
15
17
  PaymentListView,
16
- PaymentDashboardView,
17
- CurrencyConverterView,
18
+ AdminPaymentViewSet,
19
+ AdminWebhookViewSet,
20
+ AdminWebhookEventViewSet,
21
+ WebhookTestViewSet,
22
+ AdminStatsViewSet,
23
+ AdminUserViewSet,
18
24
  )
25
+ from .admin_interface.views.dashboard import WebhookDashboardView
19
26
 
20
27
  app_name = 'cfg_payments_admin'
21
28
 
29
+ # DRF Routers for Admin API
30
+ admin_router = DefaultRouter()
31
+ admin_router.register(r'payments', AdminPaymentViewSet, basename='admin-payment')
32
+ admin_router.register(r'webhooks', AdminWebhookViewSet, basename='admin-webhook')
33
+ admin_router.register(r'stats', AdminStatsViewSet, basename='admin-stats')
34
+ admin_router.register(r'users', AdminUserViewSet, basename='admin-user')
35
+
36
+ # Nested router for webhook events
37
+ webhook_events_router = routers.NestedSimpleRouter(admin_router, r'webhooks', lookup='webhook')
38
+ webhook_events_router.register(r'events', AdminWebhookEventViewSet, basename='admin-webhook-events')
39
+
40
+ # Public API router (no authentication required)
41
+ public_router = DefaultRouter()
42
+ public_router.register(r'webhooks', WebhookTestViewSet, basename='webhook-test')
43
+
22
44
  urlpatterns = [
23
- # Main dashboard
45
+ # Template Views
24
46
  path('', staff_member_required(PaymentDashboardView.as_view()), name='dashboard'),
25
47
  path('dashboard/', staff_member_required(PaymentDashboardView.as_view()), name='dashboard_alt'),
26
48
 
27
- # Payment management
49
+ # Payment management templates
28
50
  path('payments/', include([
29
51
  path('', staff_member_required(PaymentListView.as_view()), name='payment-list'),
30
- path('create/', staff_member_required(PaymentFormView.as_view()), name='payment-create'),
31
- path('<uuid:pk>/', staff_member_required(PaymentStatusView.as_view()), name='payment-detail'),
32
- path('status/<uuid:pk>/', staff_member_required(PaymentStatusView.as_view()), name='payment-status'),
52
+ path('create/', staff_member_required(PaymentFormView.as_view()), name='payment-form'),
53
+ path('<uuid:pk>/', staff_member_required(PaymentDetailView.as_view()), name='payment-detail'),
33
54
  ])),
34
55
 
35
- # Webhook management
56
+ # Webhook management templates
36
57
  path('webhooks/', include([
37
58
  path('', staff_member_required(WebhookDashboardView.as_view()), name='webhook-dashboard'),
38
59
  path('dashboard/', staff_member_required(WebhookDashboardView.as_view()), name='webhook-dashboard-alt'),
39
60
  ])),
40
61
 
41
- # Tools and utilities
42
- path('tools/', include([
43
- path('converter/', staff_member_required(CurrencyConverterView.as_view()), name='currency-converter'),
62
+ # API Routes with DRF ViewSets
63
+ path('api/', include([
64
+ path('', include(admin_router.urls)),
65
+ path('', include(webhook_events_router.urls)),
66
+ path('', include(public_router.urls)), # Public API endpoints
44
67
  ])),
45
-
46
- # Development/testing tools (only in DEBUG mode)
47
- # path('test/', PaymentTestView.as_view(), name='test'),
48
- # path('debug/', PaymentDebugView.as_view(), name='debug'),
49
68
  ]
@@ -11,6 +11,7 @@ from django_filters.rest_framework import DjangoFilterBackend
11
11
  from django.contrib.auth import get_user_model
12
12
  from django.db import models
13
13
  from django.utils import timezone
14
+ from drf_spectacular.utils import extend_schema, extend_schema_view
14
15
 
15
16
  from .base import PaymentBaseViewSet, NestedPaymentViewSet, ReadOnlyPaymentViewSet
16
17
  from ...models import APIKey
@@ -21,6 +22,7 @@ from ..serializers.api_keys import (
21
22
  APIKeyUpdateSerializer,
22
23
  APIKeyActionSerializer,
23
24
  APIKeyValidationSerializer,
25
+ APIKeyValidationResponseSerializer,
24
26
  APIKeyStatsSerializer,
25
27
  )
26
28
  from django_cfg.modules.django_logger import get_logger
@@ -78,6 +80,12 @@ class APIKeyViewSet(PaymentBaseViewSet):
78
80
 
79
81
  return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
80
82
 
83
+ @extend_schema(
84
+ summary="Validate API Key",
85
+ description="Validate an API key and return key information",
86
+ request=APIKeyValidationSerializer,
87
+ responses={200: APIKeyValidationResponseSerializer}
88
+ )
81
89
  @action(detail=False, methods=['post'])
82
90
  def validate_key(self, request):
83
91
  """
@@ -376,6 +384,12 @@ class APIKeyValidateView(generics.GenericAPIView):
376
384
  serializer_class = APIKeyValidationSerializer
377
385
  permission_classes = [permissions.IsAuthenticated]
378
386
 
387
+ @extend_schema(
388
+ summary="Validate API Key (Standalone)",
389
+ description="Standalone endpoint to validate an API key and return key information",
390
+ request=APIKeyValidationSerializer,
391
+ responses={200: APIKeyValidationResponseSerializer}
392
+ )
379
393
  def post(self, request, *args, **kwargs):
380
394
  """Validate API key."""
381
395
  serializer = self.get_serializer(data=request.data)
@@ -32,6 +32,7 @@ class PaymentBaseViewSet(viewsets.ModelViewSet):
32
32
  permission_classes = [permissions.IsAuthenticated]
33
33
  filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
34
34
  ordering = ['-created_at']
35
+ versioning_class = None # Disable versioning for payments API
35
36
 
36
37
  # Serializer classes mapping for different actions
37
38
  serializer_classes = {}
@@ -195,7 +195,7 @@ class NetworkViewSet(ReadOnlyPaymentViewSet):
195
195
  queryset = Network.objects.filter(is_active=True)
196
196
  serializer_class = NetworkSerializer
197
197
  permission_classes = [permissions.IsAuthenticated]
198
- filterset_fields = ['currency', 'is_active']
198
+ filterset_fields = ['native_currency__code', 'is_active']
199
199
  search_fields = ['name', 'code']
200
200
  ordering_fields = ['name', 'code', 'created_at']
201
201
 
@@ -251,7 +251,7 @@ class ProviderCurrencyViewSet(ReadOnlyPaymentViewSet):
251
251
  queryset = ProviderCurrency.objects.filter(is_enabled=True)
252
252
  serializer_class = ProviderCurrencySerializer
253
253
  permission_classes = [permissions.IsAuthenticated]
254
- filterset_fields = ['provider', 'currency', 'network', 'is_enabled']
254
+ filterset_fields = ['provider', 'currency__code', 'network__code', 'is_enabled']
255
255
  search_fields = ['provider_currency_code']
256
256
  ordering_fields = ['provider', 'created_at', 'min_amount', 'max_amount']
257
257
 
@@ -37,8 +37,8 @@ class PaymentViewSet(PaymentBaseViewSet):
37
37
 
38
38
  queryset = UniversalPayment.objects.all()
39
39
  serializer_class = PaymentSerializer
40
- permission_classes = [permissions.IsAdminUser] # Admin only for global access
41
- filterset_fields = ['status', 'provider', 'currency_code', 'user']
40
+ permission_classes = [permissions.IsAuthenticated] # Allow authenticated users
41
+ filterset_fields = ['status', 'provider', 'currency__code', 'user']
42
42
  search_fields = ['description', 'provider_payment_id', 'transaction_hash']
43
43
  ordering_fields = ['created_at', 'updated_at', 'amount_usd', 'expires_at']
44
44
 
@@ -52,9 +52,15 @@ class PaymentViewSet(PaymentBaseViewSet):
52
52
 
53
53
  def get_queryset(self):
54
54
  """Optimized queryset with related objects."""
55
- return super().get_queryset().select_related('user').prefetch_related(
56
- 'user__userbalance_set'
55
+ queryset = super().get_queryset().select_related('user').prefetch_related(
56
+ 'user__payment_balance'
57
57
  )
58
+
59
+ # Non-staff users can only see their own payments
60
+ if not self.request.user.is_staff:
61
+ queryset = queryset.filter(user=self.request.user)
62
+
63
+ return queryset
58
64
 
59
65
  @action(detail=True, methods=['post'])
60
66
  def check_status(self, request, pk=None):
@@ -178,7 +184,7 @@ class UserPaymentViewSet(NestedPaymentViewSet):
178
184
  queryset = UniversalPayment.objects.all()
179
185
  serializer_class = PaymentSerializer
180
186
  permission_classes = [permissions.IsAuthenticated]
181
- filterset_fields = ['status', 'provider', 'currency_code']
187
+ filterset_fields = ['status', 'provider', 'currency__code']
182
188
  search_fields = ['description', 'provider_payment_id']
183
189
  ordering_fields = ['created_at', 'updated_at', 'amount_usd', 'expires_at']
184
190
 
@@ -40,9 +40,9 @@ class SubscriptionViewSet(PaymentBaseViewSet):
40
40
 
41
41
  queryset = Subscription.objects.all()
42
42
  serializer_class = SubscriptionSerializer
43
- permission_classes = [permissions.IsAdminUser] # Admin only for global access
44
- filterset_fields = ['status', 'tier', 'tariff', 'endpoint_group', 'user']
45
- search_fields = ['user__username', 'user__email', 'tariff__name']
43
+ permission_classes = [permissions.IsAuthenticated] # Allow authenticated users
44
+ filterset_fields = ['status', 'tier', 'user']
45
+ search_fields = ['user__username', 'user__email']
46
46
  ordering_fields = ['created_at', 'updated_at', 'expires_at', 'total_requests']
47
47
 
48
48
  serializer_classes = {
@@ -55,11 +55,17 @@ class SubscriptionViewSet(PaymentBaseViewSet):
55
55
 
56
56
  def get_queryset(self):
57
57
  """Optimized queryset with related objects."""
58
- return super().get_queryset().select_related(
59
- 'user', 'tariff', 'endpoint_group'
58
+ queryset = super().get_queryset().select_related(
59
+ 'user'
60
60
  ).prefetch_related(
61
- 'tariff__endpoint_groups'
61
+ 'endpoint_groups'
62
62
  )
63
+
64
+ # Non-staff users can only see their own subscriptions
65
+ if not self.request.user.is_staff:
66
+ queryset = queryset.filter(user=self.request.user)
67
+
68
+ return queryset
63
69
 
64
70
  @action(detail=True, methods=['post'])
65
71
  def update_status(self, request, pk=None):
@@ -161,43 +167,42 @@ class SubscriptionViewSet(PaymentBaseViewSet):
161
167
  )
162
168
 
163
169
  @action(detail=False, methods=['get'])
164
- def by_tariff(self, request):
170
+ def by_tier(self, request):
165
171
  """
166
- Get subscriptions grouped by tariff.
172
+ Get subscriptions grouped by tier.
167
173
 
168
- GET /api/subscriptions/by_tariff/
174
+ GET /api/subscriptions/by_tier/
169
175
  """
170
176
  try:
171
177
  queryset = self.filter_queryset(self.get_queryset())
172
178
 
173
- tariff_stats = {}
174
- for subscription in queryset.select_related('tariff'):
175
- tariff_name = subscription.tariff.name if subscription.tariff else 'No Tariff'
176
- tariff_id = subscription.tariff.id if subscription.tariff else None
179
+ tier_stats = {}
180
+ for tier_choice in Subscription.SubscriptionTier.choices:
181
+ tier_code = tier_choice[0]
182
+ tier_name = tier_choice[1]
177
183
 
178
- if tariff_name not in tariff_stats:
179
- tariff_stats[tariff_name] = {
180
- 'tariff_id': tariff_id,
181
- 'tariff_name': tariff_name,
182
- 'total_subscriptions': 0,
183
- 'active_subscriptions': 0,
184
- 'total_requests': 0,
185
- }
184
+ tier_subscriptions = queryset.filter(tier=tier_code)
186
185
 
187
- tariff_stats[tariff_name]['total_subscriptions'] += 1
188
- if subscription.is_active():
189
- tariff_stats[tariff_name]['active_subscriptions'] += 1
190
- tariff_stats[tariff_name]['total_requests'] += subscription.total_requests or 0
186
+ tier_stats[tier_code] = {
187
+ 'name': tier_name,
188
+ 'total_subscriptions': tier_subscriptions.count(),
189
+ 'active_subscriptions': tier_subscriptions.filter(
190
+ status=Subscription.SubscriptionStatus.ACTIVE
191
+ ).count(),
192
+ 'total_requests': tier_subscriptions.aggregate(
193
+ total=models.Sum('total_requests')
194
+ )['total'] or 0,
195
+ }
191
196
 
192
197
  return Response({
193
- 'tariff_stats': tariff_stats,
198
+ 'tier_stats': tier_stats,
194
199
  'generated_at': timezone.now().isoformat()
195
200
  })
196
201
 
197
202
  except Exception as e:
198
- logger.error(f"Subscription tariff stats failed: {e}")
203
+ logger.error(f"Subscription tier stats failed: {e}")
199
204
  return Response(
200
- {'error': f'Tariff stats failed: {e}'},
205
+ {'error': f'Tier stats failed: {e}'},
201
206
  status=status.HTTP_500_INTERNAL_SERVER_ERROR
202
207
  )
203
208
 
@@ -212,8 +217,8 @@ class UserSubscriptionViewSet(NestedPaymentViewSet):
212
217
  queryset = Subscription.objects.all()
213
218
  serializer_class = SubscriptionSerializer
214
219
  permission_classes = [permissions.IsAuthenticated]
215
- filterset_fields = ['status', 'tier', 'tariff', 'endpoint_group']
216
- search_fields = ['tariff__name']
220
+ filterset_fields = ['status', 'tier']
221
+ search_fields = []
217
222
  ordering_fields = ['created_at', 'updated_at', 'expires_at']
218
223
 
219
224
  # Nested ViewSet configuration
@@ -236,7 +241,7 @@ class UserSubscriptionViewSet(NestedPaymentViewSet):
236
241
  if str(self.request.user.id) != str(user_id):
237
242
  return queryset.none()
238
243
 
239
- return queryset.select_related('tariff', 'endpoint_group')
244
+ return queryset.select_related('user').prefetch_related('endpoint_groups')
240
245
 
241
246
  @action(detail=True, methods=['post'])
242
247
  def update_status(self, request, user_pk=None, pk=None):
@@ -0,0 +1,40 @@
1
+ """
2
+ 💰 Payments Overview Dashboard
3
+
4
+ Overview dashboard for user payment metrics and analytics.
5
+ """
6
+
7
+ from .views import PaymentsDashboardViewSet
8
+ from .serializers import (
9
+ PaymentsDashboardOverviewSerializer,
10
+ PaymentsMetricsSerializer,
11
+ BalanceOverviewSerializer,
12
+ SubscriptionOverviewSerializer,
13
+ APIKeysOverviewSerializer,
14
+ PaymentsChartResponseSerializer,
15
+ )
16
+ from .services import (
17
+ PaymentsDashboardMetricsService,
18
+ PaymentsUsageChartService,
19
+ RecentPaymentsService,
20
+ PaymentsAnalyticsService,
21
+ )
22
+
23
+ __all__ = [
24
+ # Views
25
+ 'PaymentsDashboardViewSet',
26
+
27
+ # Serializers
28
+ 'PaymentsDashboardOverviewSerializer',
29
+ 'PaymentsMetricsSerializer',
30
+ 'BalanceOverviewSerializer',
31
+ 'SubscriptionOverviewSerializer',
32
+ 'APIKeysOverviewSerializer',
33
+ 'PaymentsChartResponseSerializer',
34
+
35
+ # Services
36
+ 'PaymentsDashboardMetricsService',
37
+ 'PaymentsUsageChartService',
38
+ 'RecentPaymentsService',
39
+ 'PaymentsAnalyticsService',
40
+ ]