django-cfg 1.2.25__py3-none-any.whl → 1.2.29__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 (37) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/config/providers.py +6 -16
  3. django_cfg/apps/payments/services/providers/cryptomus.py +2 -1
  4. django_cfg/apps/payments/static/payments/css/payments.css +340 -0
  5. django_cfg/apps/payments/static/payments/js/notifications.js +202 -0
  6. django_cfg/apps/payments/static/payments/js/payment-utils.js +318 -0
  7. django_cfg/apps/payments/static/payments/js/theme.js +86 -0
  8. django_cfg/apps/payments/templates/payments/base.html +182 -0
  9. django_cfg/apps/payments/templates/payments/components/payment_card.html +201 -0
  10. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +109 -0
  11. django_cfg/apps/payments/templates/payments/components/progress_bar.html +36 -0
  12. django_cfg/apps/payments/templates/payments/components/provider_stats.html +40 -0
  13. django_cfg/apps/payments/templates/payments/components/status_badge.html +27 -0
  14. django_cfg/apps/payments/templates/payments/components/status_overview.html +144 -0
  15. django_cfg/apps/payments/templates/payments/dashboard.html +346 -0
  16. django_cfg/apps/payments/templatetags/__init__.py +1 -0
  17. django_cfg/apps/payments/templatetags/payments_tags.py +315 -0
  18. django_cfg/apps/payments/urls_templates.py +52 -0
  19. django_cfg/apps/payments/views/templates/__init__.py +25 -0
  20. django_cfg/apps/payments/views/templates/ajax.py +312 -0
  21. django_cfg/apps/payments/views/templates/base.py +204 -0
  22. django_cfg/apps/payments/views/templates/dashboard.py +60 -0
  23. django_cfg/apps/payments/views/templates/payment_detail.py +102 -0
  24. django_cfg/apps/payments/views/templates/payment_management.py +164 -0
  25. django_cfg/apps/payments/views/templates/qr_code.py +174 -0
  26. django_cfg/apps/payments/views/templates/stats.py +240 -0
  27. django_cfg/apps/payments/views/templates/utils.py +181 -0
  28. django_cfg/apps/urls.py +3 -0
  29. django_cfg/models/payments.py +1 -0
  30. django_cfg/registry/core.py +1 -0
  31. django_cfg/template_archive/.gitignore +1 -0
  32. django_cfg/template_archive/django_sample.zip +0 -0
  33. {django_cfg-1.2.25.dist-info → django_cfg-1.2.29.dist-info}/METADATA +12 -15
  34. {django_cfg-1.2.25.dist-info → django_cfg-1.2.29.dist-info}/RECORD +37 -12
  35. {django_cfg-1.2.25.dist-info → django_cfg-1.2.29.dist-info}/WHEEL +0 -0
  36. {django_cfg-1.2.25.dist-info → django_cfg-1.2.29.dist-info}/entry_points.txt +0 -0
  37. {django_cfg-1.2.25.dist-info → django_cfg-1.2.29.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,102 @@
1
+ """
2
+ Payment detail view.
3
+
4
+ Provides detailed information about a single payment for superuser access.
5
+ """
6
+
7
+ from django.views.generic import DetailView
8
+ from .base import (
9
+ SuperuserRequiredMixin,
10
+ PaymentContextMixin,
11
+ log_view_access
12
+ )
13
+ from ...models import UniversalPayment, PaymentEvent
14
+
15
+
16
+ class PaymentDetailView(
17
+ SuperuserRequiredMixin,
18
+ PaymentContextMixin,
19
+ DetailView
20
+ ):
21
+ """Detailed view for a single payment."""
22
+
23
+ model = UniversalPayment
24
+ template_name = 'payments/payment_detail.html'
25
+ context_object_name = 'payment'
26
+ page_title = 'Payment Details'
27
+
28
+ def get_breadcrumbs(self):
29
+ payment = self.get_object()
30
+ return [
31
+ {'name': 'Dashboard', 'url': '/payments/admin/'},
32
+ {'name': 'Payments', 'url': '/payments/admin/list/'},
33
+ {'name': f'Payment #{payment.internal_payment_id or str(payment.id)[:8]}', 'url': ''},
34
+ ]
35
+
36
+ def get_context_data(self, **kwargs):
37
+ context = super().get_context_data(**kwargs)
38
+ payment = self.get_object()
39
+
40
+ # Log access for audit
41
+ log_view_access('payment_detail', self.request.user, payment_id=payment.id)
42
+
43
+ # Get payment events for this payment
44
+ events = PaymentEvent.objects.filter(payment=payment).order_by('-created_at')
45
+
46
+ # Get related payments (same user, similar amount range)
47
+ related_payments = UniversalPayment.objects.filter(
48
+ user=payment.user,
49
+ amount_usd__gte=payment.amount_usd * 0.8,
50
+ amount_usd__lte=payment.amount_usd * 1.2
51
+ ).exclude(id=payment.id).order_by('-created_at')[:5]
52
+
53
+ # Get provider-specific information
54
+ provider_info = self._get_provider_info(payment)
55
+
56
+ # Get common context
57
+ common_context = self.get_common_context()
58
+
59
+ context.update({
60
+ 'events': events,
61
+ 'related_payments': related_payments,
62
+ 'provider_info': provider_info,
63
+ 'can_retry': self._can_retry_payment(payment),
64
+ 'can_cancel': self._can_cancel_payment(payment),
65
+ 'can_refund': self._can_refund_payment(payment),
66
+ **common_context
67
+ })
68
+
69
+ return context
70
+
71
+ def _get_provider_info(self, payment):
72
+ """Get provider-specific information for the payment."""
73
+ info = {
74
+ 'display_name': payment.provider.title(),
75
+ 'is_crypto': payment.provider in ['nowpayments', 'cryptapi', 'cryptomus'],
76
+ 'supports_qr': payment.provider in ['nowpayments', 'cryptapi', 'cryptomus'],
77
+ 'supports_webhook': True,
78
+ }
79
+
80
+ # Add crypto-specific info
81
+ if info['is_crypto'] and payment.pay_address:
82
+ info.update({
83
+ 'crypto_address': payment.pay_address,
84
+ 'crypto_amount': payment.pay_amount,
85
+ 'crypto_currency': payment.currency_code,
86
+ 'network': getattr(payment, 'network', 'mainnet'),
87
+ 'confirmations': getattr(payment, 'confirmations_count', 0),
88
+ })
89
+
90
+ return info
91
+
92
+ def _can_retry_payment(self, payment):
93
+ """Check if payment can be retried."""
94
+ return payment.status in ['failed', 'expired']
95
+
96
+ def _can_cancel_payment(self, payment):
97
+ """Check if payment can be cancelled."""
98
+ return payment.status in ['pending', 'confirming']
99
+
100
+ def _can_refund_payment(self, payment):
101
+ """Check if payment can be refunded."""
102
+ return payment.status == 'completed'
@@ -0,0 +1,164 @@
1
+ """
2
+ Payment management views.
3
+
4
+ Provides list, create, and management functionality for payments.
5
+ """
6
+
7
+ from django.views.generic import TemplateView, ListView
8
+ from .base import (
9
+ SuperuserRequiredMixin,
10
+ PaymentFilterMixin,
11
+ PaymentContextMixin,
12
+ log_view_access
13
+ )
14
+ from ...models import UniversalPayment
15
+
16
+
17
+ class PaymentCreateView(
18
+ SuperuserRequiredMixin,
19
+ PaymentContextMixin,
20
+ TemplateView
21
+ ):
22
+ """Form view for creating a new payment."""
23
+
24
+ template_name = 'payments/payment_create.html'
25
+ page_title = 'Create Payment'
26
+
27
+ def get_breadcrumbs(self):
28
+ return [
29
+ {'name': 'Dashboard', 'url': '/payments/admin/'},
30
+ {'name': 'Payments', 'url': '/payments/admin/list/'},
31
+ {'name': 'Create Payment', 'url': ''},
32
+ ]
33
+
34
+ def get_context_data(self, **kwargs):
35
+ context = super().get_context_data(**kwargs)
36
+
37
+ # Log access for audit
38
+ log_view_access('payment_create', self.request.user)
39
+
40
+ # Get available providers
41
+ providers = self._get_available_providers()
42
+
43
+ # Get available currencies
44
+ currencies = self._get_available_currencies()
45
+
46
+ # Get common context
47
+ common_context = self.get_common_context()
48
+
49
+ context.update({
50
+ 'providers': providers,
51
+ 'currencies': currencies,
52
+ 'default_amount': 10.0, # Default test amount
53
+ **common_context
54
+ })
55
+
56
+ return context
57
+
58
+ def _get_available_providers(self):
59
+ """Get list of available payment providers."""
60
+ try:
61
+ from ...services.providers.registry import ProviderRegistry
62
+ providers = []
63
+ for provider_name, provider_class in ProviderRegistry.get_all_providers().items():
64
+ providers.append({
65
+ 'name': provider_name,
66
+ 'display_name': provider_name.title(),
67
+ 'is_crypto': provider_name in ['nowpayments', 'cryptapi', 'cryptomus'],
68
+ 'description': getattr(provider_class, '__doc__', ''),
69
+ })
70
+ return providers
71
+ except Exception:
72
+ # Fallback if registry is not available
73
+ return [
74
+ {'name': 'nowpayments', 'display_name': 'NowPayments', 'is_crypto': True},
75
+ {'name': 'cryptapi', 'display_name': 'CryptAPI', 'is_crypto': True},
76
+ {'name': 'cryptomus', 'display_name': 'Cryptomus', 'is_crypto': True},
77
+ {'name': 'stripe', 'display_name': 'Stripe', 'is_crypto': False},
78
+ ]
79
+
80
+ def _get_available_currencies(self):
81
+ """Get list of available currencies."""
82
+ from ...models import Currency
83
+
84
+ try:
85
+ # Get currencies from database
86
+ currencies = Currency.objects.filter(is_active=True).order_by('code')
87
+ return [{'code': c.code, 'name': c.name} for c in currencies]
88
+ except Exception:
89
+ # Fallback list
90
+ return [
91
+ {'code': 'USD', 'name': 'US Dollar'},
92
+ {'code': 'EUR', 'name': 'Euro'},
93
+ {'code': 'BTC', 'name': 'Bitcoin'},
94
+ {'code': 'ETH', 'name': 'Ethereum'},
95
+ {'code': 'LTC', 'name': 'Litecoin'},
96
+ ]
97
+
98
+
99
+ class PaymentListView(
100
+ SuperuserRequiredMixin,
101
+ PaymentFilterMixin,
102
+ PaymentContextMixin,
103
+ ListView
104
+ ):
105
+ """Paginated list view for all payments."""
106
+
107
+ model = UniversalPayment
108
+ template_name = 'payments/payment_list.html'
109
+ context_object_name = 'payments'
110
+ paginate_by = 20
111
+ ordering = ['-created_at']
112
+ page_title = 'All Payments'
113
+
114
+ def get_breadcrumbs(self):
115
+ return [
116
+ {'name': 'Dashboard', 'url': '/payments/admin/'},
117
+ {'name': 'All Payments', 'url': ''},
118
+ ]
119
+
120
+ def get_queryset(self):
121
+ # Log access for audit
122
+ log_view_access('payment_list', self.request.user)
123
+
124
+ # Use filter mixin to get filtered queryset
125
+ return self.get_filtered_payments().order_by(*self.ordering)
126
+
127
+ def get_context_data(self, **kwargs):
128
+ context = super().get_context_data(**kwargs)
129
+
130
+ # Get filter context
131
+ filter_context = self.get_filter_context()
132
+
133
+ # Get available filter options
134
+ filter_options = self._get_filter_options()
135
+
136
+ # Get common context
137
+ common_context = self.get_common_context()
138
+
139
+ context.update({
140
+ 'filters': filter_context,
141
+ 'filter_options': filter_options,
142
+ 'total_count': self.get_queryset().count(),
143
+ **common_context
144
+ })
145
+
146
+ return context
147
+
148
+ def _get_filter_options(self):
149
+ """Get available options for filter dropdowns."""
150
+ from django.db.models import Value
151
+ from django.db.models.functions import Concat
152
+
153
+ # Get unique statuses
154
+ statuses = UniversalPayment.objects.values_list('status', flat=True).distinct()
155
+ status_choices = [(status, status.title()) for status in statuses if status]
156
+
157
+ # Get unique providers
158
+ providers = UniversalPayment.objects.values_list('provider', flat=True).distinct()
159
+ provider_choices = [(provider, provider.title()) for provider in providers if provider]
160
+
161
+ return {
162
+ 'statuses': status_choices,
163
+ 'providers': provider_choices,
164
+ }
@@ -0,0 +1,174 @@
1
+ """
2
+ QR code views for crypto payments.
3
+
4
+ Provides QR code generation and display for cryptocurrency payments.
5
+ """
6
+
7
+ from django.views.generic import DetailView
8
+ from django.http import JsonResponse
9
+ from .base import (
10
+ SuperuserRequiredMixin,
11
+ PaymentContextMixin,
12
+ superuser_required,
13
+ log_view_access
14
+ )
15
+ from ...models import UniversalPayment
16
+
17
+
18
+ class PaymentQRCodeView(
19
+ SuperuserRequiredMixin,
20
+ PaymentContextMixin,
21
+ DetailView
22
+ ):
23
+ """QR code view for crypto payments."""
24
+
25
+ model = UniversalPayment
26
+ template_name = 'payments/payment_qr.html'
27
+ context_object_name = 'payment'
28
+ page_title = 'Payment QR Code'
29
+
30
+ def get_breadcrumbs(self):
31
+ payment = self.get_object()
32
+ return [
33
+ {'name': 'Dashboard', 'url': '/payments/admin/'},
34
+ {'name': 'Payments', 'url': '/payments/admin/list/'},
35
+ {'name': f'Payment #{payment.internal_payment_id or str(payment.id)[:8]}',
36
+ 'url': f'/payments/admin/payment/{payment.id}/'},
37
+ {'name': 'QR Code', 'url': ''},
38
+ ]
39
+
40
+ def get_context_data(self, **kwargs):
41
+ context = super().get_context_data(**kwargs)
42
+ payment = self.get_object()
43
+
44
+ # Log access for audit
45
+ log_view_access('payment_qr', self.request.user, payment_id=payment.id)
46
+
47
+ # Check if payment supports QR codes
48
+ if not self._supports_qr_code(payment):
49
+ context['error'] = "QR codes are not supported for this payment method"
50
+ return context
51
+
52
+ # Generate QR code data
53
+ qr_data = self._generate_qr_data(payment)
54
+
55
+ # Get payment instructions
56
+ instructions = self._get_payment_instructions(payment)
57
+
58
+ # Get common context
59
+ common_context = self.get_common_context()
60
+
61
+ context.update({
62
+ 'qr_data': qr_data,
63
+ 'qr_size': self.request.GET.get('size', 256),
64
+ 'instructions': instructions,
65
+ 'is_crypto': self._is_crypto_payment(payment),
66
+ 'can_copy': True,
67
+ **common_context
68
+ })
69
+
70
+ return context
71
+
72
+ def _supports_qr_code(self, payment):
73
+ """Check if payment method supports QR codes."""
74
+ crypto_providers = ['nowpayments', 'cryptapi', 'cryptomus']
75
+ return payment.provider in crypto_providers and payment.pay_address
76
+
77
+ def _is_crypto_payment(self, payment):
78
+ """Check if payment is cryptocurrency-based."""
79
+ crypto_providers = ['nowpayments', 'cryptapi', 'cryptomus']
80
+ return payment.provider in crypto_providers
81
+
82
+ def _generate_qr_data(self, payment):
83
+ """Generate QR code data for the payment."""
84
+ if not payment.pay_address:
85
+ return None
86
+
87
+ # For crypto payments, use standard format
88
+ if self._is_crypto_payment(payment):
89
+ qr_data = payment.pay_address
90
+
91
+ # Add amount if available
92
+ if payment.pay_amount:
93
+ # Use appropriate URI scheme based on currency
94
+ uri_schemes = {
95
+ 'BTC': 'bitcoin',
96
+ 'LTC': 'litecoin',
97
+ 'ETH': 'ethereum',
98
+ 'BCH': 'bitcoincash',
99
+ }
100
+
101
+ scheme = uri_schemes.get(payment.currency_code.upper(), 'crypto')
102
+ qr_data = f"{scheme}:{payment.pay_address}?amount={payment.pay_amount}"
103
+
104
+ # Add label if available
105
+ if payment.internal_payment_id:
106
+ qr_data += f"&label=Payment%20{payment.internal_payment_id}"
107
+
108
+ return qr_data
109
+
110
+ return payment.pay_address
111
+
112
+ def _get_payment_instructions(self, payment):
113
+ """Get step-by-step payment instructions."""
114
+ if not self._is_crypto_payment(payment):
115
+ return []
116
+
117
+ instructions = [
118
+ "Scan the QR code with your crypto wallet app",
119
+ f"Send exactly {payment.pay_amount} {payment.currency_code} to the address",
120
+ "Wait for network confirmations",
121
+ "Payment will be automatically confirmed"
122
+ ]
123
+
124
+ # Add provider-specific instructions
125
+ if payment.provider == 'cryptapi':
126
+ instructions.append("Minimum 1 confirmation required")
127
+ elif payment.provider == 'nowpayments':
128
+ instructions.append("Minimum 2 confirmations required")
129
+ elif payment.provider == 'cryptomus':
130
+ instructions.append("Confirmations depend on selected network")
131
+
132
+ return instructions
133
+
134
+
135
+ @superuser_required
136
+ def qr_code_data_ajax(request, payment_id):
137
+ """AJAX endpoint to get QR code data."""
138
+ try:
139
+ payment = UniversalPayment.objects.get(id=payment_id)
140
+
141
+ # Log access for audit
142
+ log_view_access('qr_ajax', request.user, payment_id=payment_id)
143
+
144
+ view = PaymentQRCodeView()
145
+
146
+ # Check if payment supports QR codes
147
+ if not view._supports_qr_code(payment):
148
+ return JsonResponse({
149
+ 'error': 'QR codes not supported for this payment method'
150
+ }, status=400)
151
+
152
+ # Generate QR data
153
+ qr_data = view._generate_qr_data(payment)
154
+
155
+ if not qr_data:
156
+ return JsonResponse({
157
+ 'error': 'Unable to generate QR code data'
158
+ }, status=400)
159
+
160
+ response_data = {
161
+ 'qr_data': qr_data,
162
+ 'payment_address': payment.pay_address,
163
+ 'payment_amount': str(payment.pay_amount) if payment.pay_amount else None,
164
+ 'currency': payment.currency_code,
165
+ 'provider': payment.provider,
166
+ 'instructions': view._get_payment_instructions(payment),
167
+ }
168
+
169
+ return JsonResponse(response_data)
170
+
171
+ except UniversalPayment.DoesNotExist:
172
+ return JsonResponse({'error': 'Payment not found'}, status=404)
173
+ except Exception as e:
174
+ return JsonResponse({'error': str(e)}, status=500)
@@ -0,0 +1,240 @@
1
+ """
2
+ Payment statistics and analytics views.
3
+
4
+ Provides comprehensive analytics for payment performance and trends.
5
+ """
6
+
7
+ from django.views.generic import TemplateView
8
+ from django.utils import timezone
9
+ from django.db.models import Q
10
+ from datetime import timedelta
11
+ from .base import (
12
+ SuperuserRequiredMixin,
13
+ PaymentStatsMixin,
14
+ PaymentContextMixin,
15
+ log_view_access
16
+ )
17
+
18
+
19
+ class PaymentStatsView(
20
+ SuperuserRequiredMixin,
21
+ PaymentStatsMixin,
22
+ PaymentContextMixin,
23
+ TemplateView
24
+ ):
25
+ """Analytics and statistics view."""
26
+
27
+ template_name = 'payments/stats.html'
28
+ page_title = 'Payment Analytics'
29
+
30
+ def get_breadcrumbs(self):
31
+ return [
32
+ {'name': 'Dashboard', 'url': '/payments/admin/'},
33
+ {'name': 'Analytics', 'url': ''},
34
+ ]
35
+
36
+ def get_context_data(self, **kwargs):
37
+ context = super().get_context_data(**kwargs)
38
+
39
+ # Log access for audit
40
+ log_view_access('payment_stats', self.request.user)
41
+
42
+ # Get time period from request (default to 30 days)
43
+ days = int(self.request.GET.get('days', 30))
44
+
45
+ # Calculate date ranges
46
+ now = timezone.now()
47
+ ranges = self._get_date_ranges(now, days)
48
+
49
+ # Get comprehensive statistics
50
+ stats = {
51
+ 'overview': self._get_overview_stats(),
52
+ 'time_periods': self._get_time_period_stats(ranges),
53
+ 'providers': self._get_detailed_provider_stats(),
54
+ 'status_distribution': self._get_status_distribution(),
55
+ 'trends': self._get_trend_data(days),
56
+ 'performance': self._get_performance_metrics(),
57
+ }
58
+
59
+ # Get common context
60
+ common_context = self.get_common_context()
61
+
62
+ context.update({
63
+ 'stats': stats,
64
+ 'selected_days': days,
65
+ 'available_periods': [7, 30, 90, 365],
66
+ 'date_ranges': ranges,
67
+ **common_context
68
+ })
69
+
70
+ return context
71
+
72
+ def _get_date_ranges(self, now, days):
73
+ """Calculate various date ranges for statistics."""
74
+ return {
75
+ 'current_period_start': now - timedelta(days=days),
76
+ 'current_period_end': now,
77
+ 'previous_period_start': now - timedelta(days=days * 2),
78
+ 'previous_period_end': now - timedelta(days=days),
79
+ 'last_7_days': now - timedelta(days=7),
80
+ 'last_30_days': now - timedelta(days=30),
81
+ 'last_year': now - timedelta(days=365),
82
+ }
83
+
84
+ def _get_overview_stats(self):
85
+ """Get overall payment statistics."""
86
+ return self.get_payment_stats()
87
+
88
+ def _get_time_period_stats(self, ranges):
89
+ """Get statistics for different time periods."""
90
+ from ...models import UniversalPayment
91
+
92
+ periods = {}
93
+
94
+ # Current period
95
+ current_qs = UniversalPayment.objects.filter(
96
+ created_at__gte=ranges['current_period_start'],
97
+ created_at__lte=ranges['current_period_end']
98
+ )
99
+ periods['current'] = self.get_payment_stats(current_qs)
100
+
101
+ # Previous period for comparison
102
+ previous_qs = UniversalPayment.objects.filter(
103
+ created_at__gte=ranges['previous_period_start'],
104
+ created_at__lte=ranges['previous_period_end']
105
+ )
106
+ periods['previous'] = self.get_payment_stats(previous_qs)
107
+
108
+ # Calculate growth rates
109
+ periods['growth'] = self._calculate_growth_rates(
110
+ periods['current'],
111
+ periods['previous']
112
+ )
113
+
114
+ # Last 7 days
115
+ last_7_qs = UniversalPayment.objects.filter(created_at__gte=ranges['last_7_days'])
116
+ periods['last_7_days'] = self.get_payment_stats(last_7_qs)
117
+
118
+ # Last 30 days
119
+ last_30_qs = UniversalPayment.objects.filter(created_at__gte=ranges['last_30_days'])
120
+ periods['last_30_days'] = self.get_payment_stats(last_30_qs)
121
+
122
+ return periods
123
+
124
+ def _get_detailed_provider_stats(self):
125
+ """Get detailed provider statistics."""
126
+ provider_stats = self.get_provider_stats()
127
+
128
+ # Add additional metrics for each provider
129
+ for stat in provider_stats:
130
+ stat['avg_amount'] = stat['volume'] / stat['count'] if stat['count'] > 0 else 0
131
+ stat['failure_rate'] = 100 - stat['success_rate']
132
+
133
+ return provider_stats
134
+
135
+ def _get_status_distribution(self):
136
+ """Get payment status distribution."""
137
+ from ...models import UniversalPayment
138
+ from django.db.models import Count
139
+
140
+ distribution = UniversalPayment.objects.values('status').annotate(
141
+ count=Count('id')
142
+ ).order_by('-count')
143
+
144
+ total = sum(item['count'] for item in distribution)
145
+
146
+ # Add percentage
147
+ for item in distribution:
148
+ item['percentage'] = (item['count'] / total * 100) if total > 0 else 0
149
+
150
+ return distribution
151
+
152
+ def _get_trend_data(self, days):
153
+ """Get trend data for charts."""
154
+ from ...models import UniversalPayment
155
+ from django.db.models import Count, Sum
156
+ from django.db.models.functions import TruncDate
157
+
158
+ end_date = timezone.now()
159
+ start_date = end_date - timedelta(days=days)
160
+
161
+ # Daily trends
162
+ daily_trends = UniversalPayment.objects.filter(
163
+ created_at__gte=start_date,
164
+ created_at__lte=end_date
165
+ ).annotate(
166
+ date=TruncDate('created_at')
167
+ ).values('date').annotate(
168
+ count=Count('id'),
169
+ volume=Sum('amount_usd'),
170
+ completed=Count('id', filter=Q(status='completed'))
171
+ ).order_by('date')
172
+
173
+ # Convert to list and add calculated fields
174
+ trends = []
175
+ for item in daily_trends:
176
+ trends.append({
177
+ 'date': item['date'].isoformat(),
178
+ 'count': item['count'],
179
+ 'volume': float(item['volume'] or 0),
180
+ 'completed': item['completed'],
181
+ 'success_rate': (item['completed'] / item['count'] * 100) if item['count'] > 0 else 0
182
+ })
183
+
184
+ return trends
185
+
186
+ def _get_performance_metrics(self):
187
+ """Get performance metrics."""
188
+ from ...models import UniversalPayment, PaymentEvent
189
+ from django.db.models import Avg, Min, Max
190
+
191
+ # Average processing time (from created to completed)
192
+ completed_payments = UniversalPayment.objects.filter(
193
+ status='completed',
194
+ completed_at__isnull=False
195
+ )
196
+
197
+ processing_times = []
198
+ for payment in completed_payments[:100]: # Sample for performance
199
+ if payment.completed_at and payment.created_at:
200
+ duration = payment.completed_at - payment.created_at
201
+ processing_times.append(duration.total_seconds())
202
+
203
+ metrics = {
204
+ 'avg_processing_time': sum(processing_times) / len(processing_times) if processing_times else 0,
205
+ 'min_processing_time': min(processing_times) if processing_times else 0,
206
+ 'max_processing_time': max(processing_times) if processing_times else 0,
207
+ }
208
+
209
+ # Convert seconds to human readable format
210
+ for key in metrics:
211
+ if metrics[key] > 0:
212
+ metrics[f"{key}_formatted"] = self._format_duration(metrics[key])
213
+ else:
214
+ metrics[f"{key}_formatted"] = "N/A"
215
+
216
+ return metrics
217
+
218
+ def _calculate_growth_rates(self, current, previous):
219
+ """Calculate growth rates between two periods."""
220
+ growth = {}
221
+
222
+ for key in ['total_count', 'total_volume', 'completed_count']:
223
+ current_val = current.get(key, 0)
224
+ previous_val = previous.get(key, 0)
225
+
226
+ if previous_val > 0:
227
+ growth[f"{key}_rate"] = ((current_val - previous_val) / previous_val) * 100
228
+ else:
229
+ growth[f"{key}_rate"] = 100 if current_val > 0 else 0
230
+
231
+ return growth
232
+
233
+ def _format_duration(self, seconds):
234
+ """Format duration in seconds to human readable format."""
235
+ if seconds < 60:
236
+ return f"{seconds:.1f}s"
237
+ elif seconds < 3600:
238
+ return f"{seconds/60:.1f}m"
239
+ else:
240
+ return f"{seconds/3600:.1f}h"