django-cfg 1.2.27__py3-none-any.whl → 1.2.31__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 (138) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin/__init__.py +3 -2
  3. django_cfg/apps/payments/admin/balance_admin.py +18 -18
  4. django_cfg/apps/payments/admin/currencies_admin.py +319 -131
  5. django_cfg/apps/payments/admin/payments_admin.py +15 -4
  6. django_cfg/apps/payments/config/module.py +2 -2
  7. django_cfg/apps/payments/config/utils.py +2 -2
  8. django_cfg/apps/payments/decorators.py +2 -2
  9. django_cfg/apps/payments/management/commands/README.md +95 -127
  10. django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
  11. django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
  12. django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
  13. django_cfg/apps/payments/managers/__init__.py +3 -2
  14. django_cfg/apps/payments/managers/balance_manager.py +2 -2
  15. django_cfg/apps/payments/managers/currency_manager.py +272 -49
  16. django_cfg/apps/payments/managers/payment_manager.py +161 -13
  17. django_cfg/apps/payments/middleware/api_access.py +2 -2
  18. django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
  19. django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
  20. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
  21. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
  22. django_cfg/apps/payments/models/__init__.py +3 -2
  23. django_cfg/apps/payments/models/currencies.py +187 -71
  24. django_cfg/apps/payments/models/payments.py +3 -2
  25. django_cfg/apps/payments/serializers/__init__.py +3 -2
  26. django_cfg/apps/payments/serializers/currencies.py +20 -12
  27. django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
  28. django_cfg/apps/payments/services/core/balance_service.py +2 -2
  29. django_cfg/apps/payments/services/core/fallback_service.py +2 -2
  30. django_cfg/apps/payments/services/core/payment_service.py +3 -6
  31. django_cfg/apps/payments/services/core/subscription_service.py +4 -7
  32. django_cfg/apps/payments/services/internal_types.py +171 -7
  33. django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
  34. django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
  35. django_cfg/apps/payments/services/providers/base.py +144 -43
  36. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
  37. django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
  38. django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
  39. django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
  40. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
  41. django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
  42. django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
  43. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
  44. django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
  45. django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
  46. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
  47. django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
  48. django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
  49. django_cfg/apps/payments/services/providers/registry.py +294 -11
  50. django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
  51. django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
  52. django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
  53. django_cfg/apps/payments/services/security/error_handler.py +6 -8
  54. django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
  55. django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
  56. django_cfg/apps/payments/signals/api_key_signals.py +2 -2
  57. django_cfg/apps/payments/signals/payment_signals.py +11 -5
  58. django_cfg/apps/payments/signals/subscription_signals.py +2 -2
  59. django_cfg/apps/payments/static/payments/css/payments.css +340 -0
  60. django_cfg/apps/payments/static/payments/js/notifications.js +202 -0
  61. django_cfg/apps/payments/static/payments/js/payment-utils.js +318 -0
  62. django_cfg/apps/payments/static/payments/js/theme.js +86 -0
  63. django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
  64. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
  65. django_cfg/apps/payments/templates/payments/base.html +182 -0
  66. django_cfg/apps/payments/templates/payments/components/payment_card.html +201 -0
  67. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +109 -0
  68. django_cfg/apps/payments/templates/payments/components/progress_bar.html +43 -0
  69. django_cfg/apps/payments/templates/payments/components/provider_stats.html +40 -0
  70. django_cfg/apps/payments/templates/payments/components/status_badge.html +34 -0
  71. django_cfg/apps/payments/templates/payments/components/status_overview.html +148 -0
  72. django_cfg/apps/payments/templates/payments/dashboard.html +258 -0
  73. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
  74. django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
  75. django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
  76. django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
  77. django_cfg/apps/payments/templates/payments/stats.html +261 -0
  78. django_cfg/apps/payments/templates/payments/test.html +213 -0
  79. django_cfg/apps/payments/templatetags/__init__.py +1 -0
  80. django_cfg/apps/payments/templatetags/payments_tags.py +315 -0
  81. django_cfg/apps/payments/urls.py +3 -1
  82. django_cfg/apps/payments/urls_admin.py +58 -0
  83. django_cfg/apps/payments/utils/__init__.py +1 -3
  84. django_cfg/apps/payments/utils/billing_utils.py +2 -2
  85. django_cfg/apps/payments/utils/config_utils.py +2 -8
  86. django_cfg/apps/payments/utils/validation_utils.py +2 -2
  87. django_cfg/apps/payments/views/__init__.py +3 -2
  88. django_cfg/apps/payments/views/currency_views.py +31 -20
  89. django_cfg/apps/payments/views/payment_views.py +2 -2
  90. django_cfg/apps/payments/views/templates/__init__.py +25 -0
  91. django_cfg/apps/payments/views/templates/ajax.py +451 -0
  92. django_cfg/apps/payments/views/templates/base.py +212 -0
  93. django_cfg/apps/payments/views/templates/dashboard.py +60 -0
  94. django_cfg/apps/payments/views/templates/payment_detail.py +102 -0
  95. django_cfg/apps/payments/views/templates/payment_management.py +158 -0
  96. django_cfg/apps/payments/views/templates/qr_code.py +174 -0
  97. django_cfg/apps/payments/views/templates/stats.py +244 -0
  98. django_cfg/apps/payments/views/templates/utils.py +181 -0
  99. django_cfg/apps/payments/views/webhook_views.py +2 -2
  100. django_cfg/apps/payments/viewsets.py +3 -2
  101. django_cfg/apps/tasks/urls.py +0 -2
  102. django_cfg/apps/tasks/urls_admin.py +14 -0
  103. django_cfg/apps/urls.py +6 -3
  104. django_cfg/core/config.py +35 -0
  105. django_cfg/models/payments.py +2 -8
  106. django_cfg/modules/django_currency/__init__.py +16 -11
  107. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  108. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  109. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  110. django_cfg/modules/django_currency/core/__init__.py +1 -7
  111. django_cfg/modules/django_currency/core/converter.py +18 -23
  112. django_cfg/modules/django_currency/core/models.py +122 -11
  113. django_cfg/modules/django_currency/database/__init__.py +4 -4
  114. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  115. django_cfg/modules/django_unfold/dashboard.py +7 -2
  116. django_cfg/registry/core.py +1 -0
  117. django_cfg/template_archive/.gitignore +1 -0
  118. django_cfg/template_archive/django_sample.zip +0 -0
  119. django_cfg/templates/admin/components/action_grid.html +9 -9
  120. django_cfg/templates/admin/components/metric_card.html +5 -5
  121. django_cfg/templates/admin/components/status_badge.html +2 -2
  122. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  123. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  124. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  125. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  126. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/METADATA +13 -18
  127. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/RECORD +130 -83
  128. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  129. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  130. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  131. django_cfg/apps/payments/services/providers/cryptomus.py +0 -310
  132. django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
  133. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  134. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  135. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  136. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
  137. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
  138. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -4,14 +4,14 @@ Configuration utilities for payments module.
4
4
  Universal utilities for working with django-cfg settings and configuration.
5
5
  """
6
6
 
7
- import logging
8
7
  from typing import Optional, Dict, Any, Type
9
8
  from django.conf import settings
10
9
 
11
10
  from django_cfg.modules.base import BaseCfgModule
12
11
  from ..config.settings import PaymentsSettings
12
+ from django_cfg.modules.django_logger import get_logger
13
13
 
14
- logger = logging.getLogger(__name__)
14
+ logger = get_logger("config_utils")
15
15
 
16
16
 
17
17
  class PaymentsConfigMixin:
@@ -227,11 +227,6 @@ class PaymentsConfigUtil:
227
227
  config = PaymentsConfigMixin.get_payments_config()
228
228
  return config.enabled
229
229
 
230
- @staticmethod
231
- def is_debug_mode() -> bool:
232
- """Check if payments module is in debug mode."""
233
- config = PaymentsConfigMixin.get_payments_config()
234
- return getattr(config, 'debug_mode', False)
235
230
 
236
231
  @staticmethod
237
232
  def reset_all_caches():
@@ -242,4 +237,3 @@ class PaymentsConfigUtil:
242
237
  # Convenience exports
243
238
  get_payments_config = PaymentsConfigUtil.get_config
244
239
  is_payments_enabled = PaymentsConfigUtil.is_payments_enabled
245
- is_debug_mode = PaymentsConfigUtil.is_debug_mode
@@ -4,7 +4,7 @@ Validation utilities for payments module.
4
4
  Basic validation functions for API keys and subscription access.
5
5
  """
6
6
 
7
- import logging
7
+ from django_cfg.modules.django_logger import get_logger
8
8
  from typing import Optional, Dict, Any
9
9
  from django.contrib.auth import get_user_model
10
10
  from django.utils import timezone
@@ -12,7 +12,7 @@ from django.utils import timezone
12
12
  from ..models import APIKey, Subscription
13
13
 
14
14
  User = get_user_model()
15
- logger = logging.getLogger(__name__)
15
+ logger = get_logger("validation_utils")
16
16
 
17
17
 
18
18
  def validate_api_key(api_key: str) -> bool:
@@ -16,7 +16,7 @@ from .api_key_views import (
16
16
  APIKeyCreateView, APIKeyValidateView
17
17
  )
18
18
  from .currency_views import (
19
- CurrencyViewSet, CurrencyNetworkViewSet,
19
+ CurrencyViewSet, NetworkViewSet, ProviderCurrencyViewSet,
20
20
  SupportedCurrenciesView, CurrencyRatesView
21
21
  )
22
22
  from .tariff_views import (
@@ -50,7 +50,8 @@ __all__ = [
50
50
 
51
51
  # Currency ViewSets & Generics
52
52
  'CurrencyViewSet',
53
- 'CurrencyNetworkViewSet',
53
+ 'NetworkViewSet',
54
+ 'ProviderCurrencyViewSet',
54
55
  'SupportedCurrenciesView',
55
56
  'CurrencyRatesView',
56
57
 
@@ -6,20 +6,20 @@ from rest_framework import viewsets, permissions, generics
6
6
  from rest_framework.decorators import action
7
7
  from rest_framework.response import Response
8
8
  from django_filters.rest_framework import DjangoFilterBackend
9
- from ..models import Currency, CurrencyNetwork
9
+ from ..models import Currency, Network, ProviderCurrency
10
10
  from ..serializers import (
11
- CurrencySerializer, CurrencyNetworkSerializer, CurrencyListSerializer
11
+ CurrencySerializer, NetworkSerializer, ProviderCurrencySerializer, CurrencyListSerializer
12
12
  )
13
13
 
14
14
 
15
15
  class CurrencyViewSet(viewsets.ReadOnlyModelViewSet):
16
16
  """Currency ViewSet: /currencies/"""
17
17
 
18
- queryset = Currency.objects.filter(is_active=True)
18
+ queryset = Currency.objects.all()
19
19
  serializer_class = CurrencySerializer
20
20
  permission_classes = [permissions.IsAuthenticated]
21
21
  filter_backends = [DjangoFilterBackend]
22
- filterset_fields = ['currency_type', 'is_active']
22
+ filterset_fields = ['currency_type']
23
23
 
24
24
  def get_serializer_class(self):
25
25
  """Use list serializer for list action."""
@@ -45,22 +45,34 @@ class CurrencyViewSet(viewsets.ReadOnlyModelViewSet):
45
45
  def networks(self, request, pk=None):
46
46
  """Get networks for specific currency."""
47
47
  currency = self.get_object()
48
- networks = CurrencyNetwork.objects.filter(
49
- currency=currency,
50
- is_active=True
51
- )
52
- serializer = CurrencyNetworkSerializer(networks, many=True)
48
+ provider_currencies = ProviderCurrency.objects.filter(
49
+ base_currency=currency,
50
+ is_enabled=True
51
+ ).select_related('network').distinct('network')
52
+
53
+ networks = [pc.network for pc in provider_currencies if pc.network]
54
+ serializer = NetworkSerializer(networks, many=True)
53
55
  return Response(serializer.data)
54
56
 
55
57
 
56
- class CurrencyNetworkViewSet(viewsets.ReadOnlyModelViewSet):
57
- """Currency Network ViewSet: /currency-networks/"""
58
+ class NetworkViewSet(viewsets.ReadOnlyModelViewSet):
59
+ """Network ViewSet: /networks/"""
60
+
61
+ queryset = Network.objects.all()
62
+ serializer_class = NetworkSerializer
63
+ permission_classes = [permissions.IsAuthenticated]
64
+ filter_backends = [DjangoFilterBackend]
65
+ filterset_fields = ['code', 'name']
66
+
67
+
68
+ class ProviderCurrencyViewSet(viewsets.ReadOnlyModelViewSet):
69
+ """Provider Currency ViewSet: /provider-currencies/"""
58
70
 
59
- queryset = CurrencyNetwork.objects.filter(is_active=True)
60
- serializer_class = CurrencyNetworkSerializer
71
+ queryset = ProviderCurrency.objects.select_related('base_currency', 'network')
72
+ serializer_class = ProviderCurrencySerializer
61
73
  permission_classes = [permissions.IsAuthenticated]
62
74
  filter_backends = [DjangoFilterBackend]
63
- filterset_fields = ['currency', 'network_code', 'is_active']
75
+ filterset_fields = ['provider_name', 'base_currency', 'network', 'is_enabled']
64
76
 
65
77
  @action(detail=False, methods=['get'])
66
78
  def by_currency(self, request):
@@ -70,7 +82,7 @@ class CurrencyNetworkViewSet(viewsets.ReadOnlyModelViewSet):
70
82
  return Response({'error': 'currency parameter required'}, status=400)
71
83
 
72
84
  try:
73
- currency = Currency.objects.get(code=currency_code, is_active=True)
85
+ currency = Currency.objects.get(code=currency_code)
74
86
  networks = self.get_queryset().filter(currency=currency)
75
87
  serializer = self.get_serializer(networks, many=True)
76
88
  return Response(serializer.data)
@@ -87,7 +99,7 @@ class SupportedCurrenciesView(generics.ListAPIView):
87
99
 
88
100
  def get_queryset(self):
89
101
  """Get active currencies."""
90
- return Currency.objects.filter(is_active=True).order_by('code')
102
+ return Currency.objects.all().order_by('code')
91
103
 
92
104
 
93
105
  class CurrencyRatesView(generics.GenericAPIView):
@@ -98,14 +110,13 @@ class CurrencyRatesView(generics.GenericAPIView):
98
110
 
99
111
  def get(self, request):
100
112
  """Get current exchange rates."""
101
- currencies = Currency.objects.filter(is_active=True)
113
+ currencies = Currency.objects.all()
102
114
 
103
115
  rates = {}
104
116
  for currency in currencies:
105
117
  rates[currency.code] = {
106
- 'usd_rate': currency.usd_rate,
107
- 'updated_at': currency.rate_updated_at,
108
- 'type': currency.currency_type,
118
+ 'name': currency.name,
119
+ 'currency_type': currency.currency_type,
109
120
  }
110
121
 
111
122
  return Response(rates)
@@ -76,8 +76,8 @@ class UserPaymentViewSet(viewsets.ModelViewSet):
76
76
 
77
77
  except Exception as e:
78
78
  # Log error but don't fail completely
79
- import logging
80
- logger = logging.getLogger(__name__)
79
+ from django_cfg.modules.django_logger import get_logger
80
+ logger = get_logger("payment_views")
81
81
  logger.error(f"Payment status check failed for {payment.id}: {e}")
82
82
 
83
83
  return Response({
@@ -0,0 +1,25 @@
1
+ """
2
+ Template views package for Payment Dashboard.
3
+
4
+ All views require superuser access as this is an internal admin tool.
5
+ """
6
+
7
+ from .dashboard import PaymentDashboardView
8
+ from .payment_detail import PaymentDetailView
9
+ from .payment_management import PaymentCreateView, PaymentListView
10
+ from .stats import PaymentStatsView
11
+ from .qr_code import PaymentQRCodeView
12
+ from .ajax import payment_status_ajax, payment_events_ajax
13
+ from .utils import PaymentTestView
14
+
15
+ __all__ = [
16
+ 'PaymentDashboardView',
17
+ 'PaymentDetailView',
18
+ 'PaymentCreateView',
19
+ 'PaymentListView',
20
+ 'PaymentStatsView',
21
+ 'PaymentQRCodeView',
22
+ 'PaymentTestView',
23
+ 'payment_status_ajax',
24
+ 'payment_events_ajax',
25
+ ]
@@ -0,0 +1,451 @@
1
+ """
2
+ AJAX endpoints for payment dashboard.
3
+
4
+ Provides real-time data updates and interactive functionality.
5
+ """
6
+
7
+ from django.http import JsonResponse
8
+ from django.shortcuts import get_object_or_404
9
+ from django.utils import timezone
10
+ from .base import superuser_required, get_progress_percentage, log_view_access
11
+ from ...models import UniversalPayment, PaymentEvent, Currency, Network, ProviderCurrency
12
+ from ...services.providers.registry import get_provider_registry
13
+ from ...services.internal_types import ProviderCurrencyOptionsResponse, CurrencyOptionModel
14
+ from django_cfg.modules.django_logger import get_logger
15
+
16
+ logger = get_logger("ajax_views")
17
+
18
+
19
+ @superuser_required
20
+ def payment_status_ajax(request, payment_id):
21
+ """AJAX endpoint for real-time payment status."""
22
+ try:
23
+ payment = get_object_or_404(UniversalPayment, id=payment_id)
24
+
25
+ # Log access for audit
26
+ log_view_access('payment_status_ajax', request.user, payment_id=payment_id)
27
+
28
+ data = {
29
+ 'id': str(payment.id),
30
+ 'status': payment.status,
31
+ 'status_display': payment.get_status_display(),
32
+ 'progress_percentage': get_progress_percentage(payment.status),
33
+ 'amount_usd': float(payment.amount_usd),
34
+ 'currency_code': payment.currency_code,
35
+ 'provider': payment.provider,
36
+ 'provider_display': payment.provider.title(),
37
+ 'created_at': payment.created_at.isoformat(),
38
+ 'updated_at': payment.updated_at.isoformat(),
39
+ }
40
+
41
+ # Add completion time if available
42
+ if payment.completed_at:
43
+ data['completed_at'] = payment.completed_at.isoformat()
44
+
45
+ # Add crypto-specific data
46
+ if payment.provider in ['nowpayments', 'cryptapi', 'cryptomus']:
47
+ data.update({
48
+ 'is_crypto': True,
49
+ 'pay_address': payment.pay_address,
50
+ 'pay_amount': str(payment.pay_amount) if payment.pay_amount else None,
51
+ 'network': getattr(payment, 'network', None),
52
+ 'confirmations': getattr(payment, 'confirmations_count', 0),
53
+ })
54
+ else:
55
+ data['is_crypto'] = False
56
+
57
+ return JsonResponse(data)
58
+
59
+ except Exception as e:
60
+ return JsonResponse({'error': str(e)}, status=400)
61
+
62
+
63
+ @superuser_required
64
+ def payment_events_ajax(request, payment_id):
65
+ """AJAX endpoint for payment events."""
66
+ try:
67
+ payment = get_object_or_404(UniversalPayment, id=payment_id)
68
+
69
+ # Log access for audit
70
+ log_view_access('payment_events_ajax', request.user, payment_id=payment_id)
71
+
72
+ events = PaymentEvent.objects.filter(payment_id=payment.id).order_by('-created_at')
73
+
74
+ events_data = []
75
+ for event in events:
76
+ event_data = {
77
+ 'id': event.id,
78
+ 'event_type': event.event_type,
79
+ 'created_at': event.created_at.isoformat(),
80
+ 'metadata': event.metadata or {},
81
+ }
82
+
83
+ # Add human-readable description
84
+ event_data['description'] = _get_event_description(event)
85
+
86
+ events_data.append(event_data)
87
+
88
+ return JsonResponse({
89
+ 'events': events_data,
90
+ 'count': len(events_data)
91
+ })
92
+
93
+ except Exception as e:
94
+ return JsonResponse({'error': str(e)}, status=400)
95
+
96
+
97
+ @superuser_required
98
+ def payment_stats_ajax(request):
99
+ """AJAX endpoint for dashboard statistics."""
100
+ try:
101
+ # Log access for audit
102
+ log_view_access('payment_stats_ajax', request.user)
103
+
104
+ from .base import PaymentStatsMixin
105
+
106
+ # Create instance to use mixin methods
107
+ stats_mixin = PaymentStatsMixin()
108
+
109
+ # Get basic stats
110
+ payment_stats = stats_mixin.get_payment_stats()
111
+ provider_stats = stats_mixin.get_provider_stats()
112
+
113
+ # Get time range stats if requested
114
+ days = int(request.GET.get('days', 30))
115
+ time_range_stats = stats_mixin.get_time_range_stats(days)
116
+
117
+ data = {
118
+ 'payment_stats': payment_stats,
119
+ 'provider_stats': list(provider_stats),
120
+ 'time_range_stats': time_range_stats,
121
+ 'last_updated': timezone.now().isoformat(),
122
+ }
123
+
124
+ return JsonResponse(data)
125
+
126
+ except Exception as e:
127
+ return JsonResponse({'error': str(e)}, status=500)
128
+
129
+
130
+ @superuser_required
131
+ def payment_search_ajax(request):
132
+ """AJAX endpoint for payment search."""
133
+ try:
134
+ query = request.GET.get('q', '').strip()
135
+ if not query:
136
+ return JsonResponse({'results': []})
137
+
138
+ # Log access for audit
139
+ log_view_access('payment_search_ajax', request.user, query=query)
140
+
141
+ from django.db.models import Q
142
+
143
+ # Search payments
144
+ payments = UniversalPayment.objects.filter(
145
+ Q(internal_payment_id__icontains=query) |
146
+ Q(provider_payment_id__icontains=query) |
147
+ Q(user__email__icontains=query) |
148
+ Q(pay_address__icontains=query)
149
+ ).order_by('-created_at')[:20]
150
+
151
+ results = []
152
+ for payment in payments:
153
+ results.append({
154
+ 'id': str(payment.id),
155
+ 'internal_id': payment.internal_payment_id,
156
+ 'provider_id': payment.provider_payment_id,
157
+ 'amount_usd': float(payment.amount_usd),
158
+ 'currency_code': payment.currency_code,
159
+ 'status': payment.status,
160
+ 'status_display': payment.get_status_display(),
161
+ 'provider': payment.provider,
162
+ 'provider_display': payment.provider.title(),
163
+ 'user_email': payment.user.email,
164
+ 'created_at': payment.created_at.isoformat(),
165
+ 'url': f'/payments/payment/{payment.id}/',
166
+ })
167
+
168
+ return JsonResponse({
169
+ 'results': results,
170
+ 'count': len(results),
171
+ 'query': query
172
+ })
173
+
174
+ except Exception as e:
175
+ return JsonResponse({'error': str(e)}, status=500)
176
+
177
+
178
+ @superuser_required
179
+ def payment_action_ajax(request, payment_id):
180
+ """AJAX endpoint for payment actions (cancel, retry, etc.)."""
181
+ try:
182
+ if request.method != 'POST':
183
+ return JsonResponse({'error': 'POST method required'}, status=405)
184
+
185
+ payment = get_object_or_404(UniversalPayment, id=payment_id)
186
+ action = request.POST.get('action')
187
+
188
+ # Log access for audit
189
+ log_view_access('payment_action_ajax', request.user,
190
+ payment_id=payment_id, action=action)
191
+
192
+ if action == 'cancel':
193
+ return _handle_cancel_payment(payment, request.user)
194
+ elif action == 'retry':
195
+ return _handle_retry_payment(payment, request.user)
196
+ elif action == 'refresh':
197
+ return _handle_refresh_payment(payment, request.user)
198
+ else:
199
+ return JsonResponse({'error': 'Invalid action'}, status=400)
200
+
201
+ except Exception as e:
202
+ return JsonResponse({'error': str(e)}, status=500)
203
+
204
+
205
+ def _get_event_description(event):
206
+ """Get human-readable description for payment event."""
207
+ descriptions = {
208
+ 'created': 'Payment was created',
209
+ 'pending': 'Payment is pending confirmation',
210
+ 'confirming': 'Payment is being confirmed',
211
+ 'confirmed': 'Payment has been confirmed',
212
+ 'completed': 'Payment was completed successfully',
213
+ 'failed': 'Payment failed',
214
+ 'cancelled': 'Payment was cancelled',
215
+ 'expired': 'Payment expired',
216
+ 'refunded': 'Payment was refunded',
217
+ 'webhook_received': 'Webhook notification received',
218
+ 'status_updated': 'Payment status was updated',
219
+ }
220
+
221
+ description = descriptions.get(event.event_type, f'Event: {event.event_type}')
222
+
223
+ # Add metadata details if available
224
+ if event.metadata:
225
+ if 'reason' in event.metadata:
226
+ description += f" - {event.metadata['reason']}"
227
+ if 'amount' in event.metadata:
228
+ description += f" (Amount: ${event.metadata['amount']})"
229
+
230
+ return description
231
+
232
+
233
+ def _handle_cancel_payment(payment, user):
234
+ """Handle payment cancellation."""
235
+ if payment.status not in ['pending', 'confirming']:
236
+ return JsonResponse({
237
+ 'error': 'Payment cannot be cancelled in current status'
238
+ }, status=400)
239
+
240
+ try:
241
+ # Update payment status
242
+ payment.status = 'cancelled'
243
+ payment.save()
244
+
245
+ # Create event
246
+ PaymentEvent.objects.create(
247
+ payment=payment,
248
+ event_type='cancelled',
249
+ metadata={'cancelled_by': user.email}
250
+ )
251
+
252
+ return JsonResponse({
253
+ 'success': True,
254
+ 'message': 'Payment cancelled successfully',
255
+ 'new_status': payment.status
256
+ })
257
+
258
+ except Exception as e:
259
+ return JsonResponse({'error': f'Failed to cancel payment: {str(e)}'}, status=500)
260
+
261
+
262
+ def _handle_retry_payment(payment, user):
263
+ """Handle payment retry."""
264
+ if payment.status not in ['failed', 'expired']:
265
+ return JsonResponse({
266
+ 'error': 'Payment cannot be retried in current status'
267
+ }, status=400)
268
+
269
+ try:
270
+ # Reset payment status
271
+ payment.status = 'pending'
272
+ payment.save()
273
+
274
+ # Create event
275
+ PaymentEvent.objects.create(
276
+ payment=payment,
277
+ event_type='retried',
278
+ metadata={'retried_by': user.email}
279
+ )
280
+
281
+ return JsonResponse({
282
+ 'success': True,
283
+ 'message': 'Payment retry initiated',
284
+ 'new_status': payment.status
285
+ })
286
+
287
+ except Exception as e:
288
+ return JsonResponse({'error': f'Failed to retry payment: {str(e)}'}, status=500)
289
+
290
+
291
+ def _handle_refresh_payment(payment, user):
292
+ """Handle payment status refresh."""
293
+ try:
294
+ # Try to refresh payment status from provider
295
+ from ...services.core.payment_service import PaymentService
296
+
297
+ payment_service = PaymentService()
298
+ updated_payment = payment_service.refresh_payment_status(payment.id)
299
+
300
+ # Create event
301
+ PaymentEvent.objects.create(
302
+ payment=payment,
303
+ event_type='refreshed',
304
+ metadata={'refreshed_by': user.email}
305
+ )
306
+
307
+ return JsonResponse({
308
+ 'success': True,
309
+ 'message': 'Payment status refreshed',
310
+ 'new_status': updated_payment.status
311
+ })
312
+
313
+ except Exception as e:
314
+ return JsonResponse({
315
+ 'success': False,
316
+ 'message': f'Failed to refresh payment: {str(e)}'
317
+ })
318
+
319
+
320
+ @superuser_required
321
+ def provider_currencies_ajax(request):
322
+ """AJAX endpoint for getting supported currencies by provider using new ProviderCurrency model."""
323
+ try:
324
+ provider_name = request.GET.get('provider')
325
+ if not provider_name:
326
+ return JsonResponse({'error': 'Provider parameter required'}, status=400)
327
+
328
+ # Log access for audit
329
+ log_view_access('provider_currencies_ajax', request.user, provider=provider_name)
330
+
331
+ # Get flat currency options using manager method
332
+ try:
333
+ currency_options_dicts = ProviderCurrency.objects.get_currency_options_for_provider(provider_name)
334
+
335
+ # Convert dicts to Pydantic models for validation
336
+ currency_options = [CurrencyOptionModel(**option) for option in currency_options_dicts]
337
+
338
+ # Create typed response
339
+ response_data = ProviderCurrencyOptionsResponse(
340
+ success=True,
341
+ provider=provider_name,
342
+ currency_options=currency_options,
343
+ count=len(currency_options)
344
+ )
345
+
346
+ return JsonResponse(response_data.model_dump())
347
+
348
+ except Exception as e:
349
+ logger.error(f"Error getting currencies for {provider_name}: {e}")
350
+
351
+ # Create typed error response
352
+ error_response = ProviderCurrencyOptionsResponse(
353
+ success=False,
354
+ provider=provider_name,
355
+ currency_options=[],
356
+ count=0,
357
+ error=f'Failed to get currencies for {provider_name}: {str(e)}'
358
+ )
359
+ return JsonResponse(error_response.model_dump())
360
+
361
+ except Exception as e:
362
+ logger.error(f"Provider currencies AJAX error: {e}")
363
+ return JsonResponse({'error': str(e)}, status=500)
364
+
365
+
366
+ # currency_networks_ajax removed - networks now included in provider_currencies_ajax
367
+
368
+
369
+ @superuser_required
370
+ def all_providers_data_ajax(request):
371
+ """AJAX endpoint for getting all providers with their currencies and capabilities."""
372
+ try:
373
+ # Log access for audit
374
+ log_view_access('all_providers_data_ajax', request.user)
375
+
376
+ # Get provider registry
377
+ registry = get_provider_registry()
378
+ all_providers = registry.list_providers()
379
+
380
+ providers_data = {}
381
+
382
+ for provider_name in all_providers:
383
+ try:
384
+ provider_instance = registry.get_provider(provider_name)
385
+
386
+ if provider_instance:
387
+ # Get provider info
388
+ provider_info = {
389
+ 'name': provider_name,
390
+ 'display_name': provider_name.title(),
391
+ 'enabled': provider_instance.enabled,
392
+ 'is_crypto': provider_name in ['nowpayments', 'cryptapi', 'cryptomus'],
393
+ 'supports_webhooks': hasattr(provider_instance, 'validate_webhook'),
394
+ 'currencies': [],
395
+ 'default_currency': None
396
+ }
397
+
398
+ # Get supported currencies from ProviderCurrency model
399
+ try:
400
+ provider_currencies = ProviderCurrency.objects.enabled_for_provider(provider_name).select_related(
401
+ 'base_currency'
402
+ ).distinct('base_currency')[:10] # Limit to first 10 for performance
403
+
404
+ for pc in provider_currencies:
405
+ provider_info['currencies'].append({
406
+ 'code': pc.base_currency.code,
407
+ 'name': pc.base_currency.name,
408
+ 'type': pc.base_currency.currency_type
409
+ })
410
+
411
+ # Set default currency
412
+ if provider_info['currencies']:
413
+ defaults = {
414
+ 'cryptapi': 'BTC',
415
+ 'cryptomus': 'USDT',
416
+ 'nowpayments': 'BTC',
417
+ 'stripe': 'USD'
418
+ }
419
+ default_code = defaults.get(provider_name)
420
+ if default_code and any(c['code'] == default_code for c in provider_info['currencies']):
421
+ provider_info['default_currency'] = default_code
422
+ else:
423
+ provider_info['default_currency'] = provider_info['currencies'][0]['code']
424
+
425
+ except Exception as e:
426
+ provider_info['error'] = f'Failed to load currencies: {str(e)}'
427
+
428
+ providers_data[provider_name] = provider_info
429
+
430
+ else:
431
+ providers_data[provider_name] = {
432
+ 'name': provider_name,
433
+ 'enabled': False,
434
+ 'error': 'Provider instance not available'
435
+ }
436
+
437
+ except Exception as e:
438
+ providers_data[provider_name] = {
439
+ 'name': provider_name,
440
+ 'enabled': False,
441
+ 'error': str(e)
442
+ }
443
+
444
+ return JsonResponse({
445
+ 'success': True,
446
+ 'providers': providers_data,
447
+ 'count': len(providers_data)
448
+ })
449
+
450
+ except Exception as e:
451
+ return JsonResponse({'error': str(e)}, status=500)