django-cfg 1.2.27__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 (35) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/services/providers/cryptomus.py +2 -1
  3. django_cfg/apps/payments/static/payments/css/payments.css +340 -0
  4. django_cfg/apps/payments/static/payments/js/notifications.js +202 -0
  5. django_cfg/apps/payments/static/payments/js/payment-utils.js +318 -0
  6. django_cfg/apps/payments/static/payments/js/theme.js +86 -0
  7. django_cfg/apps/payments/templates/payments/base.html +182 -0
  8. django_cfg/apps/payments/templates/payments/components/payment_card.html +201 -0
  9. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +109 -0
  10. django_cfg/apps/payments/templates/payments/components/progress_bar.html +36 -0
  11. django_cfg/apps/payments/templates/payments/components/provider_stats.html +40 -0
  12. django_cfg/apps/payments/templates/payments/components/status_badge.html +27 -0
  13. django_cfg/apps/payments/templates/payments/components/status_overview.html +144 -0
  14. django_cfg/apps/payments/templates/payments/dashboard.html +346 -0
  15. django_cfg/apps/payments/templatetags/__init__.py +1 -0
  16. django_cfg/apps/payments/templatetags/payments_tags.py +315 -0
  17. django_cfg/apps/payments/urls_templates.py +52 -0
  18. django_cfg/apps/payments/views/templates/__init__.py +25 -0
  19. django_cfg/apps/payments/views/templates/ajax.py +312 -0
  20. django_cfg/apps/payments/views/templates/base.py +204 -0
  21. django_cfg/apps/payments/views/templates/dashboard.py +60 -0
  22. django_cfg/apps/payments/views/templates/payment_detail.py +102 -0
  23. django_cfg/apps/payments/views/templates/payment_management.py +164 -0
  24. django_cfg/apps/payments/views/templates/qr_code.py +174 -0
  25. django_cfg/apps/payments/views/templates/stats.py +240 -0
  26. django_cfg/apps/payments/views/templates/utils.py +181 -0
  27. django_cfg/apps/urls.py +3 -0
  28. django_cfg/registry/core.py +1 -0
  29. django_cfg/template_archive/.gitignore +1 -0
  30. django_cfg/template_archive/django_sample.zip +0 -0
  31. {django_cfg-1.2.27.dist-info → django_cfg-1.2.29.dist-info}/METADATA +12 -15
  32. {django_cfg-1.2.27.dist-info → django_cfg-1.2.29.dist-info}/RECORD +35 -10
  33. {django_cfg-1.2.27.dist-info → django_cfg-1.2.29.dist-info}/WHEEL +0 -0
  34. {django_cfg-1.2.27.dist-info → django_cfg-1.2.29.dist-info}/entry_points.txt +0 -0
  35. {django_cfg-1.2.27.dist-info → django_cfg-1.2.29.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,346 @@
1
+ {% extends 'payments/base.html' %}
2
+ {% load payments_tags %}
3
+ {% load static %}
4
+
5
+ {% block title %}Payment Dashboard - Django CFG{% endblock %}
6
+
7
+ {% block header_title %}Payment Dashboard{% endblock %}
8
+ {% block header_subtitle %}Monitor and manage payment transactions in real-time{% endblock %}
9
+
10
+ {% block extra_head %}
11
+ <!-- Dashboard-specific styles -->
12
+ <style>
13
+ .dashboard-grid {
14
+ display: grid;
15
+ grid-template-columns: 1fr;
16
+ gap: 1.5rem;
17
+ }
18
+
19
+ @media (min-width: 1024px) {
20
+ .dashboard-grid {
21
+ grid-template-columns: 2fr 1fr;
22
+ }
23
+ }
24
+ </style>
25
+ {% endblock %}
26
+
27
+ {% block content %}
28
+ <div class="payment-dashboard">
29
+ <!-- Status Overview -->
30
+ {% include 'payments/components/status_overview.html' %}
31
+
32
+ <!-- Main Dashboard Grid -->
33
+ <div class="dashboard-grid">
34
+ <!-- Left Column: Recent Payments -->
35
+ <div class="space-y-6">
36
+ <!-- Filter Controls -->
37
+ <div class="payment-filters">
38
+ <div class="flex items-center justify-between mb-4">
39
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white">Recent Payments</h3>
40
+ <div class="flex items-center space-x-2">
41
+ <select class="filter-select" id="status-filter" onchange="filterPayments()">
42
+ <option value="">All Statuses</option>
43
+ <option value="pending">Pending</option>
44
+ <option value="confirming">Confirming</option>
45
+ <option value="completed">Completed</option>
46
+ <option value="failed">Failed</option>
47
+ </select>
48
+ <select class="filter-select" id="provider-filter" onchange="filterPayments()">
49
+ <option value="">All Providers</option>
50
+ <option value="nowpayments">NowPayments</option>
51
+ <option value="cryptapi">CryptAPI</option>
52
+ <option value="cryptomus">Cryptomus</option>
53
+ <option value="stripe">Stripe</option>
54
+ </select>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="filter-group">
59
+ <input type="text"
60
+ class="filter-input"
61
+ placeholder="Search by payment ID or amount..."
62
+ id="search-input"
63
+ onkeyup="searchPayments(event)">
64
+ <input type="date"
65
+ class="filter-input"
66
+ id="date-filter"
67
+ onchange="filterPayments()">
68
+ <button class="btn btn-outline btn-sm" onclick="clearFilters()">
69
+ <span class="material-icons text-sm mr-1">clear</span>
70
+ Clear
71
+ </button>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- Payment Cards Grid -->
76
+ <div id="payments-grid" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
77
+ {% if payments %}
78
+ {% for payment in payments %}
79
+ {% payment_card payment %}
80
+ {% endfor %}
81
+ {% else %}
82
+ <!-- Empty State -->
83
+ <div class="col-span-full">
84
+ <div class="text-center py-12">
85
+ <span class="material-icons text-gray-400 text-6xl mb-4">payment</span>
86
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No payments found</h3>
87
+ <p class="text-gray-500 dark:text-gray-400 mb-4">Get started by creating your first payment.</p>
88
+ <button class="btn btn-primary" onclick="createNewPayment()">
89
+ <span class="material-icons text-sm mr-2">add</span>
90
+ Create Payment
91
+ </button>
92
+ </div>
93
+ </div>
94
+ {% endif %}
95
+ </div>
96
+
97
+ <!-- Load More Button -->
98
+ {% if has_more_payments %}
99
+ <div class="text-center">
100
+ <button class="btn btn-outline" id="load-more-btn" onclick="loadMorePayments()">
101
+ <span class="material-icons text-sm mr-2">expand_more</span>
102
+ Load More Payments
103
+ </button>
104
+ </div>
105
+ {% endif %}
106
+ </div>
107
+
108
+ <!-- Right Column: Analytics & Quick Info -->
109
+ <div class="space-y-6">
110
+ <!-- Provider Statistics -->
111
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
112
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Provider Performance</h3>
113
+ {% provider_statistics %}
114
+ </div>
115
+
116
+ <!-- Recent Activity -->
117
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
118
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Recent Activity</h3>
119
+ <div class="space-y-3">
120
+ {% for event in recent_events|slice:":5" %}
121
+ <div class="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700">
122
+ <div class="flex-shrink-0">
123
+ {% if event.event_type == 'created' %}
124
+ <span class="material-icons text-blue-500">add_circle</span>
125
+ {% elif event.event_type == 'completed' %}
126
+ <span class="material-icons text-green-500">check_circle</span>
127
+ {% elif event.event_type == 'failed' %}
128
+ <span class="material-icons text-red-500">error</span>
129
+ {% else %}
130
+ <span class="material-icons text-gray-500">circle</span>
131
+ {% endif %}
132
+ </div>
133
+ <div class="flex-1 min-w-0">
134
+ <p class="text-sm font-medium text-gray-900 dark:text-white">
135
+ Payment {{ event.event_type }}
136
+ </p>
137
+ <p class="text-xs text-gray-500 dark:text-gray-400">
138
+ #{{ event.payment.internal_payment_id|default:event.payment.id|truncatechars:8 }}
139
+ • {{ event.created_at|timesince }} ago
140
+ </p>
141
+ </div>
142
+ <div class="text-sm font-medium text-gray-900 dark:text-white">
143
+ ${{ event.payment.amount_usd|floatformat:2 }}
144
+ </div>
145
+ </div>
146
+ {% empty %}
147
+ <p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
148
+ No recent activity
149
+ </p>
150
+ {% endfor %}
151
+ </div>
152
+ </div>
153
+
154
+ <!-- System Status -->
155
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
156
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">System Status</h3>
157
+ <div class="space-y-3">
158
+ <div class="flex items-center justify-between">
159
+ <span class="text-sm text-gray-500 dark:text-gray-400">API Status</span>
160
+ <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
161
+ <span class="w-2 h-2 bg-green-500 rounded-full mr-1"></span>
162
+ Online
163
+ </span>
164
+ </div>
165
+ <div class="flex items-center justify-between">
166
+ <span class="text-sm text-gray-500 dark:text-gray-400">Webhook Queue</span>
167
+ <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
168
+ Processing
169
+ </span>
170
+ </div>
171
+ <div class="flex items-center justify-between">
172
+ <span class="text-sm text-gray-500 dark:text-gray-400">Database</span>
173
+ <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
174
+ Connected
175
+ </span>
176
+ </div>
177
+ <div class="flex items-center justify-between">
178
+ <span class="text-sm text-gray-500 dark:text-gray-400">Redis Cache</span>
179
+ <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
180
+ Active
181
+ </span>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ {% endblock %}
189
+
190
+ {% block extra_js %}
191
+ <script>
192
+ // Dashboard JavaScript functionality
193
+ let currentFilters = {
194
+ status: '',
195
+ provider: '',
196
+ search: '',
197
+ date: ''
198
+ };
199
+
200
+ let currentPage = 1;
201
+ const PAYMENTS_PER_PAGE = 10;
202
+
203
+ // Filter functions
204
+ function filterPayments() {
205
+ currentFilters.status = document.getElementById('status-filter').value;
206
+ currentFilters.provider = document.getElementById('provider-filter').value;
207
+ currentFilters.date = document.getElementById('date-filter').value;
208
+
209
+ currentPage = 1;
210
+ loadPayments();
211
+ }
212
+
213
+ function searchPayments(event) {
214
+ // Debounce search input
215
+ clearTimeout(window.searchTimeout);
216
+ window.searchTimeout = setTimeout(() => {
217
+ currentFilters.search = event.target.value;
218
+ currentPage = 1;
219
+ loadPayments();
220
+ }, 500);
221
+ }
222
+
223
+ function clearFilters() {
224
+ document.getElementById('status-filter').value = '';
225
+ document.getElementById('provider-filter').value = '';
226
+ document.getElementById('search-input').value = '';
227
+ document.getElementById('date-filter').value = '';
228
+
229
+ currentFilters = { status: '', provider: '', search: '', date: '' };
230
+ currentPage = 1;
231
+ loadPayments();
232
+ }
233
+
234
+ function loadPayments() {
235
+ const grid = document.getElementById('payments-grid');
236
+
237
+ // Show loading state
238
+ if (currentPage === 1) {
239
+ grid.innerHTML = '<div class="col-span-full text-center py-8"><div class="skeleton h-32 w-full"></div></div>';
240
+ }
241
+
242
+ // Build query parameters
243
+ const params = new URLSearchParams({
244
+ page: currentPage,
245
+ page_size: PAYMENTS_PER_PAGE,
246
+ ...currentFilters
247
+ });
248
+
249
+ // Remove empty parameters
250
+ for (const [key, value] of params.entries()) {
251
+ if (!value) {
252
+ params.delete(key);
253
+ }
254
+ }
255
+
256
+ fetch(`/api/payments/list/?${params}`)
257
+ .then(response => response.json())
258
+ .then(data => {
259
+ if (currentPage === 1) {
260
+ grid.innerHTML = '';
261
+ }
262
+
263
+ if (data.results && data.results.length > 0) {
264
+ data.results.forEach(payment => {
265
+ const cardHtml = renderPaymentCard(payment);
266
+ grid.insertAdjacentHTML('beforeend', cardHtml);
267
+ });
268
+
269
+ // Update load more button
270
+ const loadMoreBtn = document.getElementById('load-more-btn');
271
+ if (loadMoreBtn) {
272
+ loadMoreBtn.style.display = data.has_next ? 'block' : 'none';
273
+ }
274
+ } else if (currentPage === 1) {
275
+ // Show empty state
276
+ grid.innerHTML = `
277
+ <div class="col-span-full text-center py-12">
278
+ <span class="material-icons text-gray-400 text-6xl mb-4">search_off</span>
279
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No payments found</h3>
280
+ <p class="text-gray-500 dark:text-gray-400">Try adjusting your filters.</p>
281
+ </div>
282
+ `;
283
+ }
284
+ })
285
+ .catch(error => {
286
+ console.error('Failed to load payments:', error);
287
+ if (window.notificationManager) {
288
+ window.notificationManager.error('Failed to load payments');
289
+ }
290
+ });
291
+ }
292
+
293
+ function loadMorePayments() {
294
+ currentPage++;
295
+ loadPayments();
296
+ }
297
+
298
+ function renderPaymentCard(payment) {
299
+ // This would be a simplified version of the payment card template
300
+ // In a real implementation, you'd want to render this server-side or use a template engine
301
+ return `
302
+ <div class="payment-card" data-payment-id="${payment.id}">
303
+ <!-- Simplified payment card HTML -->
304
+ <div class="p-4 bg-white dark:bg-gray-800 rounded-lg border">
305
+ <div class="flex justify-between items-start mb-2">
306
+ <span class="font-medium">#${payment.internal_payment_id || payment.id.substring(0, 8)}</span>
307
+ <span class="px-2 py-1 text-xs rounded-full payment-status-${payment.status}">
308
+ ${payment.status}
309
+ </span>
310
+ </div>
311
+ <div class="text-xl font-bold mb-2">$${parseFloat(payment.amount_usd).toFixed(2)}</div>
312
+ <div class="text-sm text-gray-500">${payment.provider} • ${new Date(payment.created_at).toLocaleDateString()}</div>
313
+ </div>
314
+ </div>
315
+ `;
316
+ }
317
+
318
+ // Auto-refresh dashboard every 30 seconds
319
+ setInterval(() => {
320
+ if (document.visibilityState === 'visible') {
321
+ loadPayments();
322
+ }
323
+ }, 30000);
324
+
325
+ // Initialize dashboard
326
+ document.addEventListener('DOMContentLoaded', () => {
327
+ // Update URL parameters on filter change
328
+ window.addEventListener('popstate', () => {
329
+ // Handle browser back/forward
330
+ const urlParams = new URLSearchParams(window.location.search);
331
+ currentFilters.status = urlParams.get('status') || '';
332
+ currentFilters.provider = urlParams.get('provider') || '';
333
+ currentFilters.search = urlParams.get('search') || '';
334
+ currentFilters.date = urlParams.get('date') || '';
335
+
336
+ // Update form values
337
+ document.getElementById('status-filter').value = currentFilters.status;
338
+ document.getElementById('provider-filter').value = currentFilters.provider;
339
+ document.getElementById('search-input').value = currentFilters.search;
340
+ document.getElementById('date-filter').value = currentFilters.date;
341
+
342
+ loadPayments();
343
+ });
344
+ });
345
+ </script>
346
+ {% endblock %}
@@ -0,0 +1 @@
1
+ # Template tags package for payments app
@@ -0,0 +1,315 @@
1
+ """
2
+ Payment Template Tags
3
+
4
+ Custom template tags for payment functionality, status badges, progress bars,
5
+ and real-time payment tracking components.
6
+ """
7
+
8
+ from django import template
9
+ from django.utils.safestring import mark_safe
10
+ from django.utils.html import format_html
11
+ from django.db.models import Count, Sum, Q
12
+ from decimal import Decimal
13
+ import json
14
+
15
+ register = template.Library()
16
+
17
+
18
+ @register.simple_tag
19
+ def payment_status_badge(payment):
20
+ """Render payment status badge with icon and color."""
21
+ status_config = {
22
+ 'pending': {'color': 'yellow', 'icon': 'pending', 'animate': True},
23
+ 'confirming': {'color': 'blue', 'icon': 'sync', 'animate': True},
24
+ 'confirmed': {'color': 'green', 'icon': 'check_circle', 'animate': False},
25
+ 'completed': {'color': 'green', 'icon': 'verified', 'animate': False},
26
+ 'failed': {'color': 'red', 'icon': 'error', 'animate': False},
27
+ 'expired': {'color': 'gray', 'icon': 'schedule', 'animate': False},
28
+ 'cancelled': {'color': 'gray', 'icon': 'cancel', 'animate': False},
29
+ 'refunded': {'color': 'purple', 'icon': 'undo', 'animate': False},
30
+ }
31
+
32
+ config = status_config.get(payment.status, {'color': 'gray', 'icon': 'help', 'animate': False})
33
+ animate_class = 'animate-pulse' if config['animate'] else ''
34
+
35
+ return format_html(
36
+ '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-{}-100 text-{}-800 dark:bg-{}-900 dark:text-{}-200">'
37
+ '<span class="material-icons text-sm mr-1 {}">{}</span>'
38
+ '<span class="status-text">{}</span>'
39
+ '</span>',
40
+ config['color'], config['color'], config['color'], config['color'],
41
+ animate_class,
42
+ config['icon'],
43
+ payment.get_status_display()
44
+ )
45
+
46
+
47
+ @register.filter
48
+ def payment_progress_percentage(payment):
49
+ """Calculate payment progress percentage."""
50
+ progress_map = {
51
+ 'pending': 10,
52
+ 'confirming': 40,
53
+ 'confirmed': 70,
54
+ 'completed': 100,
55
+ 'failed': 0,
56
+ 'expired': 0,
57
+ 'cancelled': 0,
58
+ 'refunded': 50, # Partial progress for refunds
59
+ }
60
+ return progress_map.get(payment.status, 0)
61
+
62
+
63
+ @register.filter
64
+ def payment_progress_steps(payment):
65
+ """Get payment progress steps with status."""
66
+ steps = [
67
+ {'label': 'Created', 'key': 'created'},
68
+ {'label': 'Processing', 'key': 'processing'},
69
+ {'label': 'Confirming', 'key': 'confirming'},
70
+ {'label': 'Completed', 'key': 'completed'},
71
+ ]
72
+
73
+ status_order = ['pending', 'confirming', 'confirmed', 'completed']
74
+ current_index = status_order.index(payment.status) if payment.status in status_order else -1
75
+
76
+ for i, step in enumerate(steps):
77
+ step['completed'] = i < current_index
78
+ step['active'] = i == current_index
79
+
80
+ return steps
81
+
82
+
83
+ @register.inclusion_tag('payments/components/payment_card.html')
84
+ def payment_card(payment, show_actions=True, compact=False):
85
+ """Render payment card component."""
86
+ return {
87
+ 'payment': payment,
88
+ 'show_actions': show_actions,
89
+ 'compact': compact
90
+ }
91
+
92
+
93
+ @register.inclusion_tag('payments/components/status_badge.html')
94
+ def render_payment_status(payment):
95
+ """Render payment status badge component."""
96
+ return {'payment': payment}
97
+
98
+
99
+ @register.inclusion_tag('payments/components/progress_bar.html')
100
+ def payment_progress_bar(payment):
101
+ """Render payment progress bar component."""
102
+ return {
103
+ 'payment': payment,
104
+ 'percentage': payment_progress_percentage(payment),
105
+ 'steps': payment_progress_steps(payment)
106
+ }
107
+
108
+
109
+ @register.inclusion_tag('payments/components/payment_tracker.html', takes_context=True)
110
+ def payment_tracker(context, payment_id):
111
+ """Render real-time payment tracker."""
112
+ try:
113
+ from ..models import UniversalPayment
114
+ payment = UniversalPayment.objects.get(id=payment_id)
115
+ return {
116
+ 'payment': payment,
117
+ 'request': context.get('request'),
118
+ 'user': context.get('user'),
119
+ 'websocket_url': f"/ws/payments/{payment_id}/"
120
+ }
121
+ except UniversalPayment.DoesNotExist:
122
+ return {'payment': None}
123
+
124
+
125
+ @register.simple_tag
126
+ def payment_websocket_url(payment_id):
127
+ """Get WebSocket URL for real-time payment updates."""
128
+ return f"/ws/payments/{payment_id}/"
129
+
130
+
131
+ @register.filter
132
+ def format_crypto_amount(amount, currency_code):
133
+ """Format cryptocurrency amount with proper decimals."""
134
+ if not amount:
135
+ return "0"
136
+
137
+ # Different currencies have different decimal places
138
+ decimal_places = {
139
+ 'BTC': 8,
140
+ 'ETH': 6,
141
+ 'LTC': 8,
142
+ 'USDT': 6,
143
+ 'USDC': 6,
144
+ 'USD': 2,
145
+ 'EUR': 2,
146
+ }
147
+
148
+ places = decimal_places.get(currency_code.upper(), 6)
149
+ formatted = f"{float(amount):.{places}f}".rstrip('0').rstrip('.')
150
+ return formatted if formatted else "0"
151
+
152
+
153
+ @register.simple_tag
154
+ def get_payment_stats():
155
+ """Get payment statistics for dashboard."""
156
+ try:
157
+ from ..models import UniversalPayment
158
+
159
+ stats = UniversalPayment.objects.aggregate(
160
+ total_count=Count('id'),
161
+ pending_count=Count('id', filter=Q(status='pending')),
162
+ confirming_count=Count('id', filter=Q(status='confirming')),
163
+ completed_count=Count('id', filter=Q(status='completed')),
164
+ failed_count=Count('id', filter=Q(status='failed')),
165
+ total_volume=Sum('amount_usd')
166
+ )
167
+
168
+ return {
169
+ 'total_payments_count': stats['total_count'] or 0,
170
+ 'pending_payments_count': stats['pending_count'] or 0,
171
+ 'confirming_payments_count': stats['confirming_count'] or 0,
172
+ 'completed_payments_count': stats['completed_count'] or 0,
173
+ 'failed_payments_count': stats['failed_count'] or 0,
174
+ 'total_volume': float(stats['total_volume'] or 0),
175
+ }
176
+ except Exception:
177
+ # Return default values if there's any error
178
+ return {
179
+ 'total_payments_count': 0,
180
+ 'pending_payments_count': 0,
181
+ 'confirming_payments_count': 0,
182
+ 'completed_payments_count': 0,
183
+ 'failed_payments_count': 0,
184
+ 'total_volume': 0.0,
185
+ }
186
+
187
+
188
+ @register.inclusion_tag('payments/components/provider_stats.html')
189
+ def provider_statistics():
190
+ """Render provider statistics."""
191
+ try:
192
+ from ..models import UniversalPayment
193
+ from django.db.models import Avg
194
+
195
+ stats = UniversalPayment.objects.values('provider').annotate(
196
+ count=Count('id'),
197
+ volume=Sum('amount_usd'),
198
+ avg_amount=Avg('amount_usd'),
199
+ completed_count=Count('id', filter=Q(status='completed')),
200
+ ).order_by('-volume')
201
+
202
+ # Calculate success rate
203
+ for stat in stats:
204
+ if stat['count'] > 0:
205
+ stat['success_rate'] = (stat['completed_count'] / stat['count']) * 100
206
+ else:
207
+ stat['success_rate'] = 0
208
+
209
+ return {'provider_stats': stats}
210
+ except Exception:
211
+ return {'provider_stats': []}
212
+
213
+
214
+ @register.simple_tag
215
+ def payment_status_distribution():
216
+ """Get payment status distribution for charts."""
217
+ try:
218
+ from ..models import UniversalPayment
219
+
220
+ distribution = UniversalPayment.objects.values('status').annotate(
221
+ count=Count('id')
222
+ ).order_by('-count')
223
+
224
+ return {item['status']: item['count'] for item in distribution}
225
+ except Exception:
226
+ return {}
227
+
228
+
229
+ @register.filter
230
+ def provider_display_name(provider_key):
231
+ """Get display name for provider."""
232
+ provider_names = {
233
+ 'nowpayments': 'NowPayments',
234
+ 'cryptapi': 'CryptAPI',
235
+ 'cryptomus': 'Cryptomus',
236
+ 'stripe': 'Stripe',
237
+ 'internal': 'Internal',
238
+ }
239
+ return provider_names.get(provider_key, provider_key.title())
240
+
241
+
242
+ @register.filter
243
+ def payment_method_icon(provider):
244
+ """Get icon for payment method."""
245
+ icons = {
246
+ 'nowpayments': 'currency_bitcoin',
247
+ 'cryptapi': 'currency_bitcoin',
248
+ 'cryptomus': 'currency_bitcoin',
249
+ 'stripe': 'credit_card',
250
+ 'internal': 'account_balance',
251
+ }
252
+ return icons.get(provider, 'payment')
253
+
254
+
255
+ @register.simple_tag
256
+ def payment_json_data(payment):
257
+ """Convert payment to JSON for JavaScript use."""
258
+ try:
259
+ data = {
260
+ 'id': str(payment.id),
261
+ 'status': payment.status,
262
+ 'amount_usd': float(payment.amount_usd),
263
+ 'currency_code': payment.currency_code,
264
+ 'provider': payment.provider,
265
+ 'created_at': payment.created_at.isoformat(),
266
+ 'progress_percentage': payment_progress_percentage(payment),
267
+ }
268
+ return mark_safe(json.dumps(data))
269
+ except Exception:
270
+ return mark_safe('{}')
271
+
272
+
273
+ @register.filter
274
+ def time_since_created(payment):
275
+ """Get human-readable time since payment was created."""
276
+ from django.utils import timezone
277
+ from django.utils.timesince import timesince
278
+
279
+ if payment.created_at:
280
+ return timesince(payment.created_at, timezone.now())
281
+ return "Unknown"
282
+
283
+
284
+ @register.filter
285
+ def is_crypto_payment(payment):
286
+ """Check if payment is cryptocurrency-based."""
287
+ crypto_providers = ['nowpayments', 'cryptapi', 'cryptomus']
288
+ return payment.provider in crypto_providers
289
+
290
+
291
+ @register.filter
292
+ def can_cancel_payment(payment):
293
+ """Check if payment can be cancelled."""
294
+ cancellable_statuses = ['pending', 'confirming']
295
+ return payment.status in cancellable_statuses
296
+
297
+
298
+ @register.filter
299
+ def payment_qr_code_data(payment):
300
+ """Get QR code data for payment."""
301
+ if hasattr(payment, 'pay_address') and payment.pay_address:
302
+ # For crypto payments, use address:amount format
303
+ if payment.pay_amount:
304
+ return f"{payment.pay_address}?amount={payment.pay_amount}"
305
+ return payment.pay_address
306
+ return None
307
+
308
+
309
+ @register.inclusion_tag('payments/components/payment_qr_code.html')
310
+ def payment_qr_code(payment):
311
+ """Render QR code for payment."""
312
+ return {
313
+ 'payment': payment,
314
+ 'qr_data': payment_qr_code_data(payment)
315
+ }