django-cfg 1.3.1__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/payments/admin_interface/old/payments/base.html +175 -0
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +125 -0
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +113 -0
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +35 -0
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +309 -0
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +518 -0
- django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/components.css +248 -9
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +163 -0
- django_cfg/apps/payments/admin_interface/serializers/__init__.py +39 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +149 -0
- django_cfg/apps/payments/admin_interface/serializers/webhook_serializers.py +114 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +55 -90
- django_cfg/apps/payments/admin_interface/templates/payments/components/dialog.html +81 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_help_dialog.html +112 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_status.html +175 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +21 -17
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +123 -250
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +170 -269
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +152 -355
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +202 -551
- django_cfg/apps/payments/admin_interface/views/__init__.py +25 -14
- django_cfg/apps/payments/admin_interface/views/api/__init__.py +20 -0
- django_cfg/apps/payments/admin_interface/views/api/payments.py +191 -0
- django_cfg/apps/payments/admin_interface/views/api/stats.py +206 -0
- django_cfg/apps/payments/admin_interface/views/api/users.py +60 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +257 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_public.py +70 -0
- django_cfg/apps/payments/admin_interface/views/base.py +114 -0
- django_cfg/apps/payments/admin_interface/views/dashboard.py +60 -0
- django_cfg/apps/payments/admin_interface/views/forms.py +94 -0
- django_cfg/apps/payments/config/helpers.py +2 -2
- django_cfg/apps/payments/management/commands/cleanup_expired_data.py +429 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +443 -0
- django_cfg/apps/payments/management/commands/manage_currencies.py +9 -20
- django_cfg/apps/payments/management/commands/manage_providers.py +5 -5
- django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
- django_cfg/apps/payments/management/commands/test_providers.py +434 -0
- django_cfg/apps/payments/middleware/api_access.py +35 -34
- django_cfg/apps/payments/migrations/0001_initial.py +1 -1
- django_cfg/apps/payments/models/balance.py +5 -2
- django_cfg/apps/payments/models/managers/api_key_managers.py +6 -2
- django_cfg/apps/payments/models/managers/balance_managers.py +3 -3
- django_cfg/apps/payments/models/managers/payment_managers.py +5 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +3 -3
- django_cfg/apps/payments/models/subscriptions.py +0 -24
- django_cfg/apps/payments/services/cache/__init__.py +1 -1
- django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
- django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
- django_cfg/apps/payments/services/cache_service/interfaces.py +32 -0
- django_cfg/apps/payments/services/cache_service/keys.py +49 -0
- django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
- django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
- django_cfg/apps/payments/services/core/balance_service.py +13 -2
- django_cfg/apps/payments/services/core/payment_service.py +49 -22
- django_cfg/apps/payments/services/integrations/ngrok_service.py +3 -3
- django_cfg/apps/payments/services/providers/registry.py +20 -0
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/balance_signals.py +8 -5
- django_cfg/apps/payments/static/payments/js/api-client.js +385 -0
- django_cfg/apps/payments/static/payments/js/ngrok-status.js +58 -0
- django_cfg/apps/payments/static/payments/js/payment-dashboard.js +50 -0
- django_cfg/apps/payments/static/payments/js/payment-form.js +175 -0
- django_cfg/apps/payments/static/payments/js/payment-list.js +95 -0
- django_cfg/apps/payments/static/payments/js/webhook-dashboard.js +154 -0
- django_cfg/apps/payments/urls.py +4 -0
- django_cfg/apps/payments/urls_admin.py +37 -18
- django_cfg/apps/payments/views/api/api_keys.py +14 -0
- django_cfg/apps/payments/views/api/base.py +1 -0
- django_cfg/apps/payments/views/api/currencies.py +2 -2
- django_cfg/apps/payments/views/api/payments.py +11 -5
- django_cfg/apps/payments/views/api/subscriptions.py +36 -31
- django_cfg/apps/payments/views/overview/__init__.py +40 -0
- django_cfg/apps/payments/views/overview/serializers.py +205 -0
- django_cfg/apps/payments/views/overview/services.py +439 -0
- django_cfg/apps/payments/views/overview/urls.py +27 -0
- django_cfg/apps/payments/views/overview/views.py +231 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +20 -6
- django_cfg/apps/payments/views/serializers/balances.py +5 -8
- django_cfg/apps/payments/views/serializers/currencies.py +2 -6
- django_cfg/apps/payments/views/serializers/payments.py +37 -32
- django_cfg/apps/payments/views/serializers/subscriptions.py +4 -26
- django_cfg/apps/urls.py +2 -1
- django_cfg/core/config.py +25 -15
- django_cfg/core/generation.py +12 -12
- django_cfg/core/integration/display/startup.py +1 -1
- django_cfg/core/validation.py +4 -4
- django_cfg/management/commands/show_config.py +2 -2
- django_cfg/management/commands/tree.py +1 -3
- django_cfg/middleware/__init__.py +2 -0
- django_cfg/middleware/static_nocache.py +55 -0
- django_cfg/models/payments.py +13 -15
- django_cfg/models/security.py +15 -0
- django_cfg/modules/django_ngrok.py +6 -0
- django_cfg/modules/django_unfold/dashboard.py +1 -3
- django_cfg/utils/smart_defaults.py +51 -5
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/METADATA +1 -1
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/RECORD +111 -69
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +0 -38
- django_cfg/apps/payments/admin_interface/views/payment_views.py +0 -259
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +0 -37
- django_cfg/apps/payments/services/cache/cache_service.py +0 -235
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/loading_spinner.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/notification.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/provider_card.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/currency_converter.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/payment_status.html +0 -0
- /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/dashboard.css +0 -0
- /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/components.js +0 -0
- /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/utils.js +0 -0
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,443 @@
|
|
1
|
+
"""
|
2
|
+
Currency Statistics Management Command for Universal Payment System v2.0.
|
3
|
+
|
4
|
+
Display comprehensive currency database statistics and health information.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from datetime import datetime, timedelta
|
8
|
+
from typing import List, Dict, Any
|
9
|
+
from decimal import Decimal
|
10
|
+
|
11
|
+
from django.core.management.base import BaseCommand, CommandError
|
12
|
+
from django.utils import timezone
|
13
|
+
from django.db.models import Q, Count, Avg, Sum, Max, Min
|
14
|
+
from django.contrib.humanize.templatetags.humanize import intcomma
|
15
|
+
|
16
|
+
from django_cfg.modules.django_logger import get_logger
|
17
|
+
from django_cfg.apps.payments.models import Currency, Network, ProviderCurrency, UniversalPayment
|
18
|
+
from django_cfg.apps.payments.services.providers.registry import get_provider_registry
|
19
|
+
|
20
|
+
logger = get_logger("currency_stats")
|
21
|
+
|
22
|
+
|
23
|
+
class Command(BaseCommand):
|
24
|
+
"""
|
25
|
+
Display currency database statistics and health information.
|
26
|
+
|
27
|
+
Features:
|
28
|
+
- Basic and detailed statistics
|
29
|
+
- Top currencies by usage and value
|
30
|
+
- Rate freshness analysis
|
31
|
+
- Provider currency coverage
|
32
|
+
- Payment volume analysis
|
33
|
+
"""
|
34
|
+
|
35
|
+
help = 'Show currency database statistics and health information'
|
36
|
+
|
37
|
+
def add_arguments(self, parser):
|
38
|
+
"""Add command line arguments."""
|
39
|
+
parser.add_argument(
|
40
|
+
'--detailed',
|
41
|
+
action='store_true',
|
42
|
+
help='Show detailed statistics breakdown'
|
43
|
+
)
|
44
|
+
|
45
|
+
parser.add_argument(
|
46
|
+
'--top',
|
47
|
+
type=int,
|
48
|
+
default=10,
|
49
|
+
help='Number of top currencies to show (default: 10)'
|
50
|
+
)
|
51
|
+
|
52
|
+
parser.add_argument(
|
53
|
+
'--check-rates',
|
54
|
+
action='store_true',
|
55
|
+
help='Check for outdated exchange rates'
|
56
|
+
)
|
57
|
+
|
58
|
+
parser.add_argument(
|
59
|
+
'--provider',
|
60
|
+
type=str,
|
61
|
+
help='Show statistics for specific provider'
|
62
|
+
)
|
63
|
+
|
64
|
+
parser.add_argument(
|
65
|
+
'--export-csv',
|
66
|
+
type=str,
|
67
|
+
help='Export statistics to CSV file'
|
68
|
+
)
|
69
|
+
|
70
|
+
parser.add_argument(
|
71
|
+
'--format',
|
72
|
+
choices=['table', 'json', 'yaml'],
|
73
|
+
default='table',
|
74
|
+
help='Output format (default: table)'
|
75
|
+
)
|
76
|
+
|
77
|
+
# Additional arguments expected by tests
|
78
|
+
parser.add_argument(
|
79
|
+
'--days',
|
80
|
+
type=int,
|
81
|
+
default=30,
|
82
|
+
help='Filter data for last N days (default: 30)'
|
83
|
+
)
|
84
|
+
|
85
|
+
parser.add_argument(
|
86
|
+
'--currency',
|
87
|
+
type=str,
|
88
|
+
help='Show statistics for specific currency code'
|
89
|
+
)
|
90
|
+
|
91
|
+
parser.add_argument(
|
92
|
+
'--verbose',
|
93
|
+
action='store_true',
|
94
|
+
help='Show verbose output (same as --detailed)'
|
95
|
+
)
|
96
|
+
|
97
|
+
def handle(self, *args, **options):
|
98
|
+
"""Execute the command."""
|
99
|
+
try:
|
100
|
+
self.options = options
|
101
|
+
self.show_header()
|
102
|
+
|
103
|
+
# Handle --verbose as alias for --detailed
|
104
|
+
if options['verbose']:
|
105
|
+
options['detailed'] = True
|
106
|
+
|
107
|
+
if options['check_rates']:
|
108
|
+
self.check_rate_freshness()
|
109
|
+
elif options['provider']:
|
110
|
+
self.show_provider_stats(options['provider'])
|
111
|
+
elif options['currency']:
|
112
|
+
self.show_currency_stats(options['currency'])
|
113
|
+
else:
|
114
|
+
self.show_general_stats()
|
115
|
+
|
116
|
+
if options['detailed']:
|
117
|
+
self.show_detailed_stats()
|
118
|
+
|
119
|
+
self.show_top_currencies()
|
120
|
+
|
121
|
+
if options['export_csv']:
|
122
|
+
self.export_to_csv(options['export_csv'])
|
123
|
+
|
124
|
+
except Exception as e:
|
125
|
+
logger.error(f"Currency stats command failed: {e}")
|
126
|
+
raise CommandError(f"Failed to generate currency statistics: {e}")
|
127
|
+
|
128
|
+
def show_header(self):
|
129
|
+
"""Display command header."""
|
130
|
+
self.stdout.write(
|
131
|
+
self.style.SUCCESS("=" * 60)
|
132
|
+
)
|
133
|
+
self.stdout.write(
|
134
|
+
self.style.SUCCESS("📊 CURRENCY DATABASE STATISTICS")
|
135
|
+
)
|
136
|
+
self.stdout.write(
|
137
|
+
self.style.SUCCESS("=" * 60)
|
138
|
+
)
|
139
|
+
self.stdout.write(f"Generated: {timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
140
|
+
self.stdout.write("")
|
141
|
+
|
142
|
+
def show_general_stats(self):
|
143
|
+
"""Show general currency statistics."""
|
144
|
+
# Basic counts
|
145
|
+
total_currencies = Currency.objects.count()
|
146
|
+
active_currencies = Currency.objects.filter(is_active=True).count()
|
147
|
+
crypto_currencies = Currency.objects.filter(currency_type='crypto').count()
|
148
|
+
fiat_currencies = Currency.objects.filter(currency_type='fiat').count()
|
149
|
+
|
150
|
+
# Network stats
|
151
|
+
total_networks = Network.objects.count()
|
152
|
+
active_networks = Network.objects.filter(is_active=True).count()
|
153
|
+
|
154
|
+
# Provider currency stats
|
155
|
+
total_provider_currencies = ProviderCurrency.objects.count()
|
156
|
+
active_provider_currencies = ProviderCurrency.objects.filter(is_enabled=True).count()
|
157
|
+
|
158
|
+
# Payment stats (filtered by days if specified)
|
159
|
+
payment_filter = Q()
|
160
|
+
if self.options.get('days'):
|
161
|
+
days_ago = timezone.now() - timedelta(days=self.options['days'])
|
162
|
+
payment_filter = Q(created_at__gte=days_ago)
|
163
|
+
|
164
|
+
total_payments = UniversalPayment.objects.filter(payment_filter).count()
|
165
|
+
completed_payments = UniversalPayment.objects.filter(payment_filter, status='completed').count()
|
166
|
+
|
167
|
+
self.stdout.write(self.style.SUCCESS("📈 GENERAL STATISTICS"))
|
168
|
+
self.stdout.write("-" * 40)
|
169
|
+
|
170
|
+
stats = [
|
171
|
+
("Total Currencies", total_currencies),
|
172
|
+
("Active Currencies", active_currencies),
|
173
|
+
("Cryptocurrency", crypto_currencies),
|
174
|
+
("Fiat Currency", fiat_currencies),
|
175
|
+
("Networks", f"{active_networks}/{total_networks}"),
|
176
|
+
("Provider Currencies", f"{active_provider_currencies}/{total_provider_currencies}"),
|
177
|
+
("Total Payments", total_payments),
|
178
|
+
("Completed Payments", completed_payments),
|
179
|
+
]
|
180
|
+
|
181
|
+
for label, value in stats:
|
182
|
+
self.stdout.write(f"{label:<20}: {self.style.WARNING(str(value))}")
|
183
|
+
|
184
|
+
self.stdout.write("")
|
185
|
+
|
186
|
+
def show_detailed_stats(self):
|
187
|
+
"""Show detailed statistics breakdown."""
|
188
|
+
self.stdout.write(self.style.SUCCESS("🔍 DETAILED BREAKDOWN"))
|
189
|
+
self.stdout.write("-" * 40)
|
190
|
+
|
191
|
+
# Currency type breakdown
|
192
|
+
crypto_active = Currency.objects.filter(currency_type='crypto', is_active=True).count()
|
193
|
+
crypto_inactive = Currency.objects.filter(currency_type='crypto', is_active=False).count()
|
194
|
+
fiat_active = Currency.objects.filter(currency_type='fiat', is_active=True).count()
|
195
|
+
fiat_inactive = Currency.objects.filter(currency_type='fiat', is_active=False).count()
|
196
|
+
|
197
|
+
self.stdout.write("Currency Status:")
|
198
|
+
self.stdout.write(f" Crypto: {crypto_active} active, {crypto_inactive} inactive")
|
199
|
+
self.stdout.write(f" Fiat: {fiat_active} active, {fiat_inactive} inactive")
|
200
|
+
|
201
|
+
# Provider coverage
|
202
|
+
providers = get_provider_registry().get_available_providers()
|
203
|
+
self.stdout.write(f"\nProvider Coverage:")
|
204
|
+
|
205
|
+
for provider_name in providers:
|
206
|
+
provider_currencies = ProviderCurrency.objects.filter(
|
207
|
+
provider=provider_name,
|
208
|
+
is_active=True
|
209
|
+
).count()
|
210
|
+
self.stdout.write(f" {provider_name}: {provider_currencies} currencies")
|
211
|
+
|
212
|
+
# Payment volume by currency
|
213
|
+
payment_stats = UniversalPayment.objects.filter(
|
214
|
+
status='completed'
|
215
|
+
).values('currency__code').annotate(
|
216
|
+
count=Count('id'),
|
217
|
+
total_volume=Sum('amount_usd')
|
218
|
+
).order_by('-total_volume')[:5]
|
219
|
+
|
220
|
+
if payment_stats:
|
221
|
+
self.stdout.write(f"\nTop Payment Currencies:")
|
222
|
+
for stat in payment_stats:
|
223
|
+
currency = stat['currency__code'] or 'Unknown'
|
224
|
+
count = stat['count']
|
225
|
+
volume = stat['total_volume'] or 0
|
226
|
+
self.stdout.write(f" {currency}: {count} payments, ${intcomma(volume)}")
|
227
|
+
|
228
|
+
self.stdout.write("")
|
229
|
+
|
230
|
+
def show_top_currencies(self):
|
231
|
+
"""Show top currencies by various metrics."""
|
232
|
+
top_count = self.options['top']
|
233
|
+
|
234
|
+
self.stdout.write(self.style.SUCCESS(f"🏆 TOP {top_count} CURRENCIES"))
|
235
|
+
self.stdout.write("-" * 40)
|
236
|
+
|
237
|
+
# Top by payment count
|
238
|
+
top_by_payments = UniversalPayment.objects.filter(
|
239
|
+
status='completed'
|
240
|
+
).values('currency__code', 'currency__name').annotate(
|
241
|
+
payment_count=Count('id')
|
242
|
+
).order_by('-payment_count')[:top_count]
|
243
|
+
|
244
|
+
if top_by_payments:
|
245
|
+
self.stdout.write("By Payment Count:")
|
246
|
+
for i, currency in enumerate(top_by_payments, 1):
|
247
|
+
code = currency['currency__code'] or 'Unknown'
|
248
|
+
name = currency['currency__name'] or 'Unknown'
|
249
|
+
count = currency['payment_count']
|
250
|
+
self.stdout.write(f" {i:2d}. {code} ({name}): {count} payments")
|
251
|
+
|
252
|
+
# Top by volume
|
253
|
+
top_by_volume = UniversalPayment.objects.filter(
|
254
|
+
status='completed'
|
255
|
+
).values('currency__code', 'currency__name').annotate(
|
256
|
+
total_volume=Sum('amount_usd')
|
257
|
+
).order_by('-total_volume')[:top_count]
|
258
|
+
|
259
|
+
if top_by_volume:
|
260
|
+
self.stdout.write(f"\nBy Payment Volume:")
|
261
|
+
for i, currency in enumerate(top_by_volume, 1):
|
262
|
+
code = currency['currency__code'] or 'Unknown'
|
263
|
+
name = currency['currency__name'] or 'Unknown'
|
264
|
+
volume = currency['total_volume'] or 0
|
265
|
+
self.stdout.write(f" {i:2d}. {code} ({name}): ${intcomma(volume)}")
|
266
|
+
|
267
|
+
self.stdout.write("")
|
268
|
+
|
269
|
+
def show_currency_stats(self, currency_code: str):
|
270
|
+
"""Show statistics for specific currency."""
|
271
|
+
self.stdout.write(self.style.SUCCESS(f"💰 CURRENCY STATISTICS: {currency_code.upper()}"))
|
272
|
+
self.stdout.write("-" * 40)
|
273
|
+
|
274
|
+
try:
|
275
|
+
currency = Currency.objects.get(code=currency_code.upper())
|
276
|
+
except Currency.DoesNotExist:
|
277
|
+
self.stdout.write(self.style.ERROR(f"Currency {currency_code} not found"))
|
278
|
+
return
|
279
|
+
|
280
|
+
# Basic currency info
|
281
|
+
self.stdout.write(f"Name: {currency.name}")
|
282
|
+
self.stdout.write(f"Type: {currency.currency_type}")
|
283
|
+
self.stdout.write(f"Active: {'Yes' if currency.is_active else 'No'}")
|
284
|
+
self.stdout.write(f"Created: {currency.created_at.strftime('%Y-%m-%d')}")
|
285
|
+
self.stdout.write(f"Updated: {currency.updated_at.strftime('%Y-%m-%d')}")
|
286
|
+
|
287
|
+
# Payment statistics
|
288
|
+
payments = UniversalPayment.objects.filter(currency=currency)
|
289
|
+
completed_payments = payments.filter(status='completed')
|
290
|
+
|
291
|
+
if payments.exists():
|
292
|
+
total_volume = completed_payments.aggregate(Sum('amount_usd'))['amount_usd__sum'] or 0
|
293
|
+
avg_payment = completed_payments.aggregate(Avg('amount_usd'))['amount_usd__avg'] or 0
|
294
|
+
|
295
|
+
self.stdout.write(f"\nPayment Statistics:")
|
296
|
+
self.stdout.write(f" Total Payments: {payments.count()}")
|
297
|
+
self.stdout.write(f" Completed: {completed_payments.count()}")
|
298
|
+
self.stdout.write(f" Total Volume: ${intcomma(total_volume)}")
|
299
|
+
self.stdout.write(f" Average Payment: ${intcomma(f'{avg_payment:.2f}')}")
|
300
|
+
else:
|
301
|
+
self.stdout.write(f"\nNo payments found for {currency_code}")
|
302
|
+
|
303
|
+
self.stdout.write("")
|
304
|
+
|
305
|
+
def check_rate_freshness(self):
|
306
|
+
"""Check for outdated exchange rates."""
|
307
|
+
self.stdout.write(self.style.SUCCESS("🕐 RATE FRESHNESS CHECK"))
|
308
|
+
self.stdout.write("-" * 40)
|
309
|
+
|
310
|
+
now = timezone.now()
|
311
|
+
stale_threshold = now - timedelta(hours=24)
|
312
|
+
very_stale_threshold = now - timedelta(days=7)
|
313
|
+
|
314
|
+
# Check currencies with stale rates
|
315
|
+
stale_currencies = Currency.objects.filter(
|
316
|
+
updated_at__lt=stale_threshold,
|
317
|
+
is_active=True
|
318
|
+
).order_by('updated_at')
|
319
|
+
|
320
|
+
very_stale_currencies = Currency.objects.filter(
|
321
|
+
updated_at__lt=very_stale_threshold,
|
322
|
+
is_active=True
|
323
|
+
).order_by('updated_at')
|
324
|
+
|
325
|
+
fresh_currencies = Currency.objects.filter(
|
326
|
+
updated_at__gte=stale_threshold,
|
327
|
+
is_active=True
|
328
|
+
).count()
|
329
|
+
|
330
|
+
self.stdout.write(f"Fresh rates (< 24h): {self.style.SUCCESS(fresh_currencies)}")
|
331
|
+
self.stdout.write(f"Stale rates (> 24h): {self.style.WARNING(stale_currencies.count())}")
|
332
|
+
self.stdout.write(f"Very stale (> 7d): {self.style.ERROR(very_stale_currencies.count())}")
|
333
|
+
|
334
|
+
if very_stale_currencies.exists():
|
335
|
+
self.stdout.write(f"\n{self.style.ERROR('⚠️ VERY STALE CURRENCIES:')}")
|
336
|
+
for currency in very_stale_currencies[:10]:
|
337
|
+
age = now - currency.updated_at
|
338
|
+
self.stdout.write(f" {currency.code}: {age.days} days old")
|
339
|
+
|
340
|
+
if stale_currencies.exists() and not very_stale_currencies.exists():
|
341
|
+
self.stdout.write(f"\n{self.style.WARNING('⚠️ STALE CURRENCIES:')}")
|
342
|
+
for currency in stale_currencies[:10]:
|
343
|
+
age = now - currency.updated_at
|
344
|
+
hours = int(age.total_seconds() / 3600)
|
345
|
+
self.stdout.write(f" {currency.code}: {hours} hours old")
|
346
|
+
|
347
|
+
self.stdout.write("")
|
348
|
+
|
349
|
+
def show_provider_stats(self, provider_name: str):
|
350
|
+
"""Show statistics for specific provider."""
|
351
|
+
self.stdout.write(self.style.SUCCESS(f"🏢 PROVIDER STATISTICS: {provider_name.upper()}"))
|
352
|
+
self.stdout.write("-" * 40)
|
353
|
+
|
354
|
+
# Provider currency stats
|
355
|
+
provider_currencies = ProviderCurrency.objects.filter(provider=provider_name)
|
356
|
+
active_provider_currencies = provider_currencies.filter(is_active=True)
|
357
|
+
|
358
|
+
# Payment stats for this provider
|
359
|
+
provider_payments = UniversalPayment.objects.filter(provider=provider_name)
|
360
|
+
completed_payments = provider_payments.filter(status='completed')
|
361
|
+
|
362
|
+
# Volume stats
|
363
|
+
total_volume = completed_payments.aggregate(Sum('amount_usd'))['amount_usd__sum'] or 0
|
364
|
+
avg_payment = completed_payments.aggregate(Avg('amount_usd'))['amount_usd__avg'] or 0
|
365
|
+
|
366
|
+
stats = [
|
367
|
+
("Total Currencies", provider_currencies.count()),
|
368
|
+
("Active Currencies", active_provider_currencies.count()),
|
369
|
+
("Total Payments", provider_payments.count()),
|
370
|
+
("Completed Payments", completed_payments.count()),
|
371
|
+
("Total Volume", f"${intcomma(total_volume)}"),
|
372
|
+
("Average Payment", f"${intcomma(f'{avg_payment:.2f}')}"),
|
373
|
+
]
|
374
|
+
|
375
|
+
for label, value in stats:
|
376
|
+
self.stdout.write(f"{label:<20}: {self.style.WARNING(str(value))}")
|
377
|
+
|
378
|
+
# Top currencies for this provider
|
379
|
+
top_currencies = completed_payments.values(
|
380
|
+
'currency__code', 'currency__name'
|
381
|
+
).annotate(
|
382
|
+
count=Count('id'),
|
383
|
+
volume=Sum('amount_usd')
|
384
|
+
).order_by('-volume')[:5]
|
385
|
+
|
386
|
+
if top_currencies:
|
387
|
+
self.stdout.write(f"\nTop Currencies:")
|
388
|
+
for currency in top_currencies:
|
389
|
+
code = currency['currency__code'] or 'Unknown'
|
390
|
+
count = currency['count']
|
391
|
+
volume = currency['volume'] or 0
|
392
|
+
self.stdout.write(f" {code}: {count} payments, ${intcomma(volume)}")
|
393
|
+
|
394
|
+
self.stdout.write("")
|
395
|
+
|
396
|
+
def export_to_csv(self, filename: str):
|
397
|
+
"""Export statistics to CSV file."""
|
398
|
+
import csv
|
399
|
+
from pathlib import Path
|
400
|
+
|
401
|
+
try:
|
402
|
+
# Collect all statistics
|
403
|
+
currencies = Currency.objects.select_related().all()
|
404
|
+
|
405
|
+
with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
|
406
|
+
writer = csv.writer(csvfile)
|
407
|
+
|
408
|
+
# Write header
|
409
|
+
writer.writerow([
|
410
|
+
'Code', 'Name', 'Type', 'Active', 'Created', 'Updated',
|
411
|
+
'Payment Count', 'Total Volume USD'
|
412
|
+
])
|
413
|
+
|
414
|
+
# Write currency data
|
415
|
+
for currency in currencies:
|
416
|
+
# Get payment stats for this currency
|
417
|
+
payments = UniversalPayment.objects.filter(
|
418
|
+
currency=currency,
|
419
|
+
status='completed'
|
420
|
+
)
|
421
|
+
payment_count = payments.count()
|
422
|
+
total_volume = payments.aggregate(Sum('amount_usd'))['amount_usd__sum'] or 0
|
423
|
+
|
424
|
+
writer.writerow([
|
425
|
+
currency.code,
|
426
|
+
currency.name,
|
427
|
+
currency.currency_type,
|
428
|
+
currency.is_active,
|
429
|
+
currency.created_at.strftime('%Y-%m-%d'),
|
430
|
+
currency.updated_at.strftime('%Y-%m-%d'),
|
431
|
+
payment_count,
|
432
|
+
f"{total_volume:.2f}"
|
433
|
+
])
|
434
|
+
|
435
|
+
self.stdout.write(
|
436
|
+
self.style.SUCCESS(f"✅ Statistics exported to: {filename}")
|
437
|
+
)
|
438
|
+
|
439
|
+
except Exception as e:
|
440
|
+
logger.error(f"Failed to export CSV: {e}")
|
441
|
+
self.stdout.write(
|
442
|
+
self.style.ERROR(f"❌ Failed to export CSV: {e}")
|
443
|
+
)
|
@@ -262,16 +262,9 @@ class Command(BaseCommand):
|
|
262
262
|
|
263
263
|
# Filter by staleness unless forced
|
264
264
|
if not options['force']:
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
currencies_needing_update = Currency.objects.filter(
|
269
|
-
Q(providercurrency__usd_rate__isnull=True) |
|
270
|
-
Q(providercurrency__rate_updated_at__isnull=True) |
|
271
|
-
Q(providercurrency__rate_updated_at__lt=stale_threshold)
|
272
|
-
).distinct()
|
273
|
-
|
274
|
-
queryset = queryset.filter(id__in=currencies_needing_update)
|
265
|
+
# For now, skip staleness check since rate fields don't exist
|
266
|
+
# TODO: Implement proper rate tracking fields
|
267
|
+
pass
|
275
268
|
|
276
269
|
# Apply limit
|
277
270
|
queryset = queryset[:options['limit']]
|
@@ -300,20 +293,16 @@ class Command(BaseCommand):
|
|
300
293
|
# Update rate in ProviderCurrency (create if doesn't exist)
|
301
294
|
provider_currency, created = ProviderCurrency.objects.get_or_create(
|
302
295
|
currency=currency,
|
303
|
-
|
296
|
+
provider='system', # System-level rate
|
304
297
|
provider_currency_code=currency.code,
|
305
298
|
defaults={
|
306
|
-
'
|
307
|
-
'rate_updated_at': timezone.now(),
|
308
|
-
'is_enabled': True,
|
309
|
-
'is_stable': currency.currency_type == Currency.CurrencyType.FIAT
|
299
|
+
'is_enabled': True
|
310
300
|
}
|
311
301
|
)
|
312
302
|
|
313
303
|
if not created:
|
314
|
-
|
315
|
-
provider_currency.
|
316
|
-
provider_currency.save(update_fields=['usd_rate', 'rate_updated_at'])
|
304
|
+
# TODO: Add rate tracking fields to ProviderCurrency model
|
305
|
+
provider_currency.save() # Touch the record to update timestamp
|
317
306
|
|
318
307
|
# Update currency's exchange rate source
|
319
308
|
currency.exchange_rate_source = 'django_currency'
|
@@ -364,9 +353,9 @@ class Command(BaseCommand):
|
|
364
353
|
crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
|
365
354
|
active_count = Currency.objects.filter(is_active=True).count()
|
366
355
|
|
367
|
-
# Count currencies with
|
356
|
+
# Count currencies with provider configs (simplified since rate fields don't exist)
|
368
357
|
currencies_with_rates = Currency.objects.filter(
|
369
|
-
|
358
|
+
provider_configs__isnull=False
|
370
359
|
).distinct().count()
|
371
360
|
|
372
361
|
rate_coverage = (currencies_with_rates / total_currencies * 100) if total_currencies > 0 else 0
|
@@ -305,18 +305,18 @@ class Command(BaseCommand):
|
|
305
305
|
self.stdout.write(f" Enabled: {enabled_provider_currencies}")
|
306
306
|
|
307
307
|
# Stats by provider
|
308
|
-
from django.db.models import Count
|
308
|
+
from django.db.models import Count, Q
|
309
309
|
|
310
|
-
provider_stats = ProviderCurrency.objects.values('
|
310
|
+
provider_stats = ProviderCurrency.objects.values('provider').annotate(
|
311
311
|
total=Count('id'),
|
312
|
-
enabled=Count('id', filter=
|
312
|
+
enabled=Count('id', filter=Q(is_enabled=True))
|
313
313
|
).order_by('-total')
|
314
314
|
|
315
315
|
if provider_stats:
|
316
316
|
self.stdout.write(f"\n📈 By Provider:")
|
317
317
|
for stat in provider_stats:
|
318
318
|
self.stdout.write(
|
319
|
-
f" - {stat['
|
319
|
+
f" - {stat['provider']}: {stat['total']} total, {stat['enabled']} enabled"
|
320
320
|
)
|
321
321
|
|
322
322
|
# Recent activity
|
@@ -330,7 +330,7 @@ class Command(BaseCommand):
|
|
330
330
|
|
331
331
|
# Rate coverage
|
332
332
|
currencies_with_rates = ProviderCurrency.objects.filter(
|
333
|
-
|
333
|
+
is_enabled=True
|
334
334
|
).count()
|
335
335
|
|
336
336
|
rate_coverage = (currencies_with_rates / total_provider_currencies * 100) if total_provider_currencies > 0 else 0
|