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.
Files changed (115) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin_interface/old/payments/base.html +175 -0
  3. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +125 -0
  4. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +113 -0
  5. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +35 -0
  6. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +309 -0
  7. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +303 -0
  8. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +382 -0
  9. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +518 -0
  10. django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/components.css +248 -9
  11. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +163 -0
  12. django_cfg/apps/payments/admin_interface/serializers/__init__.py +39 -0
  13. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +149 -0
  14. django_cfg/apps/payments/admin_interface/serializers/webhook_serializers.py +114 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/base.html +55 -90
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/dialog.html +81 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_help_dialog.html +112 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_status.html +175 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +21 -17
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +123 -250
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +170 -269
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +152 -355
  23. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +202 -551
  24. django_cfg/apps/payments/admin_interface/views/__init__.py +25 -14
  25. django_cfg/apps/payments/admin_interface/views/api/__init__.py +20 -0
  26. django_cfg/apps/payments/admin_interface/views/api/payments.py +191 -0
  27. django_cfg/apps/payments/admin_interface/views/api/stats.py +206 -0
  28. django_cfg/apps/payments/admin_interface/views/api/users.py +60 -0
  29. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +257 -0
  30. django_cfg/apps/payments/admin_interface/views/api/webhook_public.py +70 -0
  31. django_cfg/apps/payments/admin_interface/views/base.py +114 -0
  32. django_cfg/apps/payments/admin_interface/views/dashboard.py +60 -0
  33. django_cfg/apps/payments/admin_interface/views/forms.py +94 -0
  34. django_cfg/apps/payments/config/helpers.py +2 -2
  35. django_cfg/apps/payments/management/commands/cleanup_expired_data.py +429 -0
  36. django_cfg/apps/payments/management/commands/currency_stats.py +443 -0
  37. django_cfg/apps/payments/management/commands/manage_currencies.py +9 -20
  38. django_cfg/apps/payments/management/commands/manage_providers.py +5 -5
  39. django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
  40. django_cfg/apps/payments/management/commands/test_providers.py +434 -0
  41. django_cfg/apps/payments/middleware/api_access.py +35 -34
  42. django_cfg/apps/payments/migrations/0001_initial.py +1 -1
  43. django_cfg/apps/payments/models/balance.py +5 -2
  44. django_cfg/apps/payments/models/managers/api_key_managers.py +6 -2
  45. django_cfg/apps/payments/models/managers/balance_managers.py +3 -3
  46. django_cfg/apps/payments/models/managers/payment_managers.py +5 -0
  47. django_cfg/apps/payments/models/managers/subscription_managers.py +3 -3
  48. django_cfg/apps/payments/models/subscriptions.py +0 -24
  49. django_cfg/apps/payments/services/cache/__init__.py +1 -1
  50. django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
  51. django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
  52. django_cfg/apps/payments/services/cache_service/interfaces.py +32 -0
  53. django_cfg/apps/payments/services/cache_service/keys.py +49 -0
  54. django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
  55. django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
  56. django_cfg/apps/payments/services/core/balance_service.py +13 -2
  57. django_cfg/apps/payments/services/core/payment_service.py +49 -22
  58. django_cfg/apps/payments/services/integrations/ngrok_service.py +3 -3
  59. django_cfg/apps/payments/services/providers/registry.py +20 -0
  60. django_cfg/apps/payments/signals/api_key_signals.py +2 -2
  61. django_cfg/apps/payments/signals/balance_signals.py +8 -5
  62. django_cfg/apps/payments/static/payments/js/api-client.js +385 -0
  63. django_cfg/apps/payments/static/payments/js/ngrok-status.js +58 -0
  64. django_cfg/apps/payments/static/payments/js/payment-dashboard.js +50 -0
  65. django_cfg/apps/payments/static/payments/js/payment-form.js +175 -0
  66. django_cfg/apps/payments/static/payments/js/payment-list.js +95 -0
  67. django_cfg/apps/payments/static/payments/js/webhook-dashboard.js +154 -0
  68. django_cfg/apps/payments/urls.py +4 -0
  69. django_cfg/apps/payments/urls_admin.py +37 -18
  70. django_cfg/apps/payments/views/api/api_keys.py +14 -0
  71. django_cfg/apps/payments/views/api/base.py +1 -0
  72. django_cfg/apps/payments/views/api/currencies.py +2 -2
  73. django_cfg/apps/payments/views/api/payments.py +11 -5
  74. django_cfg/apps/payments/views/api/subscriptions.py +36 -31
  75. django_cfg/apps/payments/views/overview/__init__.py +40 -0
  76. django_cfg/apps/payments/views/overview/serializers.py +205 -0
  77. django_cfg/apps/payments/views/overview/services.py +439 -0
  78. django_cfg/apps/payments/views/overview/urls.py +27 -0
  79. django_cfg/apps/payments/views/overview/views.py +231 -0
  80. django_cfg/apps/payments/views/serializers/api_keys.py +20 -6
  81. django_cfg/apps/payments/views/serializers/balances.py +5 -8
  82. django_cfg/apps/payments/views/serializers/currencies.py +2 -6
  83. django_cfg/apps/payments/views/serializers/payments.py +37 -32
  84. django_cfg/apps/payments/views/serializers/subscriptions.py +4 -26
  85. django_cfg/apps/urls.py +2 -1
  86. django_cfg/core/config.py +25 -15
  87. django_cfg/core/generation.py +12 -12
  88. django_cfg/core/integration/display/startup.py +1 -1
  89. django_cfg/core/validation.py +4 -4
  90. django_cfg/management/commands/show_config.py +2 -2
  91. django_cfg/management/commands/tree.py +1 -3
  92. django_cfg/middleware/__init__.py +2 -0
  93. django_cfg/middleware/static_nocache.py +55 -0
  94. django_cfg/models/payments.py +13 -15
  95. django_cfg/models/security.py +15 -0
  96. django_cfg/modules/django_ngrok.py +6 -0
  97. django_cfg/modules/django_unfold/dashboard.py +1 -3
  98. django_cfg/utils/smart_defaults.py +51 -5
  99. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/METADATA +1 -1
  100. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/RECORD +111 -69
  101. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +0 -38
  102. django_cfg/apps/payments/admin_interface/views/payment_views.py +0 -259
  103. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +0 -37
  104. django_cfg/apps/payments/services/cache/cache_service.py +0 -235
  105. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/loading_spinner.html +0 -0
  106. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/notification.html +0 -0
  107. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/provider_card.html +0 -0
  108. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/currency_converter.html +0 -0
  109. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/payment_status.html +0 -0
  110. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/dashboard.css +0 -0
  111. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/components.js +0 -0
  112. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/utils.js +0 -0
  113. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/WHEEL +0 -0
  114. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/entry_points.txt +0 -0
  115. {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
- stale_threshold = timezone.now() - timedelta(hours=12)
266
-
267
- # Get currencies that need rate updates through ProviderCurrency
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
- provider_name='system', # System-level rate
296
+ provider='system', # System-level rate
304
297
  provider_currency_code=currency.code,
305
298
  defaults={
306
- 'usd_rate': usd_rate,
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
- provider_currency.usd_rate = usd_rate
315
- provider_currency.rate_updated_at = timezone.now()
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 rates
356
+ # Count currencies with provider configs (simplified since rate fields don't exist)
368
357
  currencies_with_rates = Currency.objects.filter(
369
- providercurrency__usd_rate__isnull=False
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('provider_name').annotate(
310
+ provider_stats = ProviderCurrency.objects.values('provider').annotate(
311
311
  total=Count('id'),
312
- enabled=Count('id', filter=models.Q(is_enabled=True))
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['provider_name']}: {stat['total']} total, {stat['enabled']} enabled"
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
- usd_rate__isnull=False
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