django-cfg 1.3.1__py3-none-any.whl → 1.3.3__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/management/commands/cleanup_expired_data.py +419 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +376 -0
- 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/models/balance.py +5 -2
- django_cfg/apps/payments/models/managers/api_key_managers.py +2 -2
- django_cfg/apps/payments/models/managers/balance_managers.py +3 -3
- django_cfg/apps/payments/models/managers/subscription_managers.py +3 -3
- 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/payment_service.py +49 -22
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/balance_signals.py +1 -1
- django_cfg/utils/smart_defaults.py +10 -4
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/METADATA +1 -1
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/RECORD +24 -15
- django_cfg/apps/payments/services/cache/cache_service.py +0 -235
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,376 @@
|
|
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
|
+
def handle(self, *args, **options):
|
78
|
+
"""Execute the command."""
|
79
|
+
try:
|
80
|
+
self.options = options
|
81
|
+
self.show_header()
|
82
|
+
|
83
|
+
if options['check_rates']:
|
84
|
+
self.check_rate_freshness()
|
85
|
+
elif options['provider']:
|
86
|
+
self.show_provider_stats(options['provider'])
|
87
|
+
else:
|
88
|
+
self.show_general_stats()
|
89
|
+
|
90
|
+
if options['detailed']:
|
91
|
+
self.show_detailed_stats()
|
92
|
+
|
93
|
+
self.show_top_currencies()
|
94
|
+
|
95
|
+
if options['export_csv']:
|
96
|
+
self.export_to_csv(options['export_csv'])
|
97
|
+
|
98
|
+
except Exception as e:
|
99
|
+
logger.error(f"Currency stats command failed: {e}")
|
100
|
+
raise CommandError(f"Failed to generate currency statistics: {e}")
|
101
|
+
|
102
|
+
def show_header(self):
|
103
|
+
"""Display command header."""
|
104
|
+
self.stdout.write(
|
105
|
+
self.style.SUCCESS("=" * 60)
|
106
|
+
)
|
107
|
+
self.stdout.write(
|
108
|
+
self.style.SUCCESS("📊 CURRENCY DATABASE STATISTICS")
|
109
|
+
)
|
110
|
+
self.stdout.write(
|
111
|
+
self.style.SUCCESS("=" * 60)
|
112
|
+
)
|
113
|
+
self.stdout.write(f"Generated: {timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
114
|
+
self.stdout.write("")
|
115
|
+
|
116
|
+
def show_general_stats(self):
|
117
|
+
"""Show general currency statistics."""
|
118
|
+
# Basic counts
|
119
|
+
total_currencies = Currency.objects.count()
|
120
|
+
active_currencies = Currency.objects.filter(is_active=True).count()
|
121
|
+
crypto_currencies = Currency.objects.filter(currency_type='crypto').count()
|
122
|
+
fiat_currencies = Currency.objects.filter(currency_type='fiat').count()
|
123
|
+
|
124
|
+
# Network stats
|
125
|
+
total_networks = Network.objects.count()
|
126
|
+
active_networks = Network.objects.filter(is_active=True).count()
|
127
|
+
|
128
|
+
# Provider currency stats
|
129
|
+
total_provider_currencies = ProviderCurrency.objects.count()
|
130
|
+
active_provider_currencies = ProviderCurrency.objects.filter(is_active=True).count()
|
131
|
+
|
132
|
+
# Payment stats
|
133
|
+
total_payments = UniversalPayment.objects.count()
|
134
|
+
completed_payments = UniversalPayment.objects.filter(status='completed').count()
|
135
|
+
|
136
|
+
self.stdout.write(self.style.SUCCESS("📈 GENERAL STATISTICS"))
|
137
|
+
self.stdout.write("-" * 40)
|
138
|
+
|
139
|
+
stats = [
|
140
|
+
("Total Currencies", total_currencies),
|
141
|
+
("Active Currencies", active_currencies),
|
142
|
+
("Cryptocurrency", crypto_currencies),
|
143
|
+
("Fiat Currency", fiat_currencies),
|
144
|
+
("Networks", f"{active_networks}/{total_networks}"),
|
145
|
+
("Provider Currencies", f"{active_provider_currencies}/{total_provider_currencies}"),
|
146
|
+
("Total Payments", total_payments),
|
147
|
+
("Completed Payments", completed_payments),
|
148
|
+
]
|
149
|
+
|
150
|
+
for label, value in stats:
|
151
|
+
self.stdout.write(f"{label:<20}: {self.style.WARNING(str(value))}")
|
152
|
+
|
153
|
+
self.stdout.write("")
|
154
|
+
|
155
|
+
def show_detailed_stats(self):
|
156
|
+
"""Show detailed statistics breakdown."""
|
157
|
+
self.stdout.write(self.style.SUCCESS("🔍 DETAILED BREAKDOWN"))
|
158
|
+
self.stdout.write("-" * 40)
|
159
|
+
|
160
|
+
# Currency type breakdown
|
161
|
+
crypto_active = Currency.objects.filter(currency_type='crypto', is_active=True).count()
|
162
|
+
crypto_inactive = Currency.objects.filter(currency_type='crypto', is_active=False).count()
|
163
|
+
fiat_active = Currency.objects.filter(currency_type='fiat', is_active=True).count()
|
164
|
+
fiat_inactive = Currency.objects.filter(currency_type='fiat', is_active=False).count()
|
165
|
+
|
166
|
+
self.stdout.write("Currency Status:")
|
167
|
+
self.stdout.write(f" Crypto: {crypto_active} active, {crypto_inactive} inactive")
|
168
|
+
self.stdout.write(f" Fiat: {fiat_active} active, {fiat_inactive} inactive")
|
169
|
+
|
170
|
+
# Provider coverage
|
171
|
+
providers = get_provider_registry().get_available_providers()
|
172
|
+
self.stdout.write(f"\nProvider Coverage:")
|
173
|
+
|
174
|
+
for provider_name in providers:
|
175
|
+
provider_currencies = ProviderCurrency.objects.filter(
|
176
|
+
provider=provider_name,
|
177
|
+
is_active=True
|
178
|
+
).count()
|
179
|
+
self.stdout.write(f" {provider_name}: {provider_currencies} currencies")
|
180
|
+
|
181
|
+
# Payment volume by currency
|
182
|
+
payment_stats = UniversalPayment.objects.filter(
|
183
|
+
status='completed'
|
184
|
+
).values('currency__code').annotate(
|
185
|
+
count=Count('id'),
|
186
|
+
total_volume=Sum('amount_usd')
|
187
|
+
).order_by('-total_volume')[:5]
|
188
|
+
|
189
|
+
if payment_stats:
|
190
|
+
self.stdout.write(f"\nTop Payment Currencies:")
|
191
|
+
for stat in payment_stats:
|
192
|
+
currency = stat['currency__code'] or 'Unknown'
|
193
|
+
count = stat['count']
|
194
|
+
volume = stat['total_volume'] or 0
|
195
|
+
self.stdout.write(f" {currency}: {count} payments, ${intcomma(volume)}")
|
196
|
+
|
197
|
+
self.stdout.write("")
|
198
|
+
|
199
|
+
def show_top_currencies(self):
|
200
|
+
"""Show top currencies by various metrics."""
|
201
|
+
top_count = self.options['top']
|
202
|
+
|
203
|
+
self.stdout.write(self.style.SUCCESS(f"🏆 TOP {top_count} CURRENCIES"))
|
204
|
+
self.stdout.write("-" * 40)
|
205
|
+
|
206
|
+
# Top by payment count
|
207
|
+
top_by_payments = UniversalPayment.objects.filter(
|
208
|
+
status='completed'
|
209
|
+
).values('currency__code', 'currency__name').annotate(
|
210
|
+
payment_count=Count('id')
|
211
|
+
).order_by('-payment_count')[:top_count]
|
212
|
+
|
213
|
+
if top_by_payments:
|
214
|
+
self.stdout.write("By Payment Count:")
|
215
|
+
for i, currency in enumerate(top_by_payments, 1):
|
216
|
+
code = currency['currency__code'] or 'Unknown'
|
217
|
+
name = currency['currency__name'] or 'Unknown'
|
218
|
+
count = currency['payment_count']
|
219
|
+
self.stdout.write(f" {i:2d}. {code} ({name}): {count} payments")
|
220
|
+
|
221
|
+
# Top by volume
|
222
|
+
top_by_volume = UniversalPayment.objects.filter(
|
223
|
+
status='completed'
|
224
|
+
).values('currency__code', 'currency__name').annotate(
|
225
|
+
total_volume=Sum('amount_usd')
|
226
|
+
).order_by('-total_volume')[:top_count]
|
227
|
+
|
228
|
+
if top_by_volume:
|
229
|
+
self.stdout.write(f"\nBy Payment Volume:")
|
230
|
+
for i, currency in enumerate(top_by_volume, 1):
|
231
|
+
code = currency['currency__code'] or 'Unknown'
|
232
|
+
name = currency['currency__name'] or 'Unknown'
|
233
|
+
volume = currency['total_volume'] or 0
|
234
|
+
self.stdout.write(f" {i:2d}. {code} ({name}): ${intcomma(volume)}")
|
235
|
+
|
236
|
+
self.stdout.write("")
|
237
|
+
|
238
|
+
def check_rate_freshness(self):
|
239
|
+
"""Check for outdated exchange rates."""
|
240
|
+
self.stdout.write(self.style.SUCCESS("🕐 RATE FRESHNESS CHECK"))
|
241
|
+
self.stdout.write("-" * 40)
|
242
|
+
|
243
|
+
now = timezone.now()
|
244
|
+
stale_threshold = now - timedelta(hours=24)
|
245
|
+
very_stale_threshold = now - timedelta(days=7)
|
246
|
+
|
247
|
+
# Check currencies with stale rates
|
248
|
+
stale_currencies = Currency.objects.filter(
|
249
|
+
updated_at__lt=stale_threshold,
|
250
|
+
is_active=True
|
251
|
+
).order_by('updated_at')
|
252
|
+
|
253
|
+
very_stale_currencies = Currency.objects.filter(
|
254
|
+
updated_at__lt=very_stale_threshold,
|
255
|
+
is_active=True
|
256
|
+
).order_by('updated_at')
|
257
|
+
|
258
|
+
fresh_currencies = Currency.objects.filter(
|
259
|
+
updated_at__gte=stale_threshold,
|
260
|
+
is_active=True
|
261
|
+
).count()
|
262
|
+
|
263
|
+
self.stdout.write(f"Fresh rates (< 24h): {self.style.SUCCESS(fresh_currencies)}")
|
264
|
+
self.stdout.write(f"Stale rates (> 24h): {self.style.WARNING(stale_currencies.count())}")
|
265
|
+
self.stdout.write(f"Very stale (> 7d): {self.style.ERROR(very_stale_currencies.count())}")
|
266
|
+
|
267
|
+
if very_stale_currencies.exists():
|
268
|
+
self.stdout.write(f"\n{self.style.ERROR('⚠️ VERY STALE CURRENCIES:')}")
|
269
|
+
for currency in very_stale_currencies[:10]:
|
270
|
+
age = now - currency.updated_at
|
271
|
+
self.stdout.write(f" {currency.code}: {age.days} days old")
|
272
|
+
|
273
|
+
if stale_currencies.exists() and not very_stale_currencies.exists():
|
274
|
+
self.stdout.write(f"\n{self.style.WARNING('⚠️ STALE CURRENCIES:')}")
|
275
|
+
for currency in stale_currencies[:10]:
|
276
|
+
age = now - currency.updated_at
|
277
|
+
hours = int(age.total_seconds() / 3600)
|
278
|
+
self.stdout.write(f" {currency.code}: {hours} hours old")
|
279
|
+
|
280
|
+
self.stdout.write("")
|
281
|
+
|
282
|
+
def show_provider_stats(self, provider_name: str):
|
283
|
+
"""Show statistics for specific provider."""
|
284
|
+
self.stdout.write(self.style.SUCCESS(f"🏢 PROVIDER STATISTICS: {provider_name.upper()}"))
|
285
|
+
self.stdout.write("-" * 40)
|
286
|
+
|
287
|
+
# Provider currency stats
|
288
|
+
provider_currencies = ProviderCurrency.objects.filter(provider=provider_name)
|
289
|
+
active_provider_currencies = provider_currencies.filter(is_active=True)
|
290
|
+
|
291
|
+
# Payment stats for this provider
|
292
|
+
provider_payments = UniversalPayment.objects.filter(provider=provider_name)
|
293
|
+
completed_payments = provider_payments.filter(status='completed')
|
294
|
+
|
295
|
+
# Volume stats
|
296
|
+
total_volume = completed_payments.aggregate(Sum('amount_usd'))['amount_usd__sum'] or 0
|
297
|
+
avg_payment = completed_payments.aggregate(Avg('amount_usd'))['amount_usd__avg'] or 0
|
298
|
+
|
299
|
+
stats = [
|
300
|
+
("Total Currencies", provider_currencies.count()),
|
301
|
+
("Active Currencies", active_provider_currencies.count()),
|
302
|
+
("Total Payments", provider_payments.count()),
|
303
|
+
("Completed Payments", completed_payments.count()),
|
304
|
+
("Total Volume", f"${intcomma(total_volume)}"),
|
305
|
+
("Average Payment", f"${intcomma(avg_payment):.2f}"),
|
306
|
+
]
|
307
|
+
|
308
|
+
for label, value in stats:
|
309
|
+
self.stdout.write(f"{label:<20}: {self.style.WARNING(str(value))}")
|
310
|
+
|
311
|
+
# Top currencies for this provider
|
312
|
+
top_currencies = completed_payments.values(
|
313
|
+
'currency__code', 'currency__name'
|
314
|
+
).annotate(
|
315
|
+
count=Count('id'),
|
316
|
+
volume=Sum('amount_usd')
|
317
|
+
).order_by('-volume')[:5]
|
318
|
+
|
319
|
+
if top_currencies:
|
320
|
+
self.stdout.write(f"\nTop Currencies:")
|
321
|
+
for currency in top_currencies:
|
322
|
+
code = currency['currency__code'] or 'Unknown'
|
323
|
+
count = currency['count']
|
324
|
+
volume = currency['volume'] or 0
|
325
|
+
self.stdout.write(f" {code}: {count} payments, ${intcomma(volume)}")
|
326
|
+
|
327
|
+
self.stdout.write("")
|
328
|
+
|
329
|
+
def export_to_csv(self, filename: str):
|
330
|
+
"""Export statistics to CSV file."""
|
331
|
+
import csv
|
332
|
+
from pathlib import Path
|
333
|
+
|
334
|
+
try:
|
335
|
+
# Collect all statistics
|
336
|
+
currencies = Currency.objects.select_related().all()
|
337
|
+
|
338
|
+
with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
|
339
|
+
writer = csv.writer(csvfile)
|
340
|
+
|
341
|
+
# Write header
|
342
|
+
writer.writerow([
|
343
|
+
'Code', 'Name', 'Type', 'Active', 'Created', 'Updated',
|
344
|
+
'Payment Count', 'Total Volume USD'
|
345
|
+
])
|
346
|
+
|
347
|
+
# Write currency data
|
348
|
+
for currency in currencies:
|
349
|
+
# Get payment stats for this currency
|
350
|
+
payments = UniversalPayment.objects.filter(
|
351
|
+
currency=currency,
|
352
|
+
status='completed'
|
353
|
+
)
|
354
|
+
payment_count = payments.count()
|
355
|
+
total_volume = payments.aggregate(Sum('amount_usd'))['amount_usd__sum'] or 0
|
356
|
+
|
357
|
+
writer.writerow([
|
358
|
+
currency.code,
|
359
|
+
currency.name,
|
360
|
+
currency.currency_type,
|
361
|
+
currency.is_active,
|
362
|
+
currency.created_at.strftime('%Y-%m-%d'),
|
363
|
+
currency.updated_at.strftime('%Y-%m-%d'),
|
364
|
+
payment_count,
|
365
|
+
f"{total_volume:.2f}"
|
366
|
+
])
|
367
|
+
|
368
|
+
self.stdout.write(
|
369
|
+
self.style.SUCCESS(f"✅ Statistics exported to: {filename}")
|
370
|
+
)
|
371
|
+
|
372
|
+
except Exception as e:
|
373
|
+
logger.error(f"Failed to export CSV: {e}")
|
374
|
+
self.stdout.write(
|
375
|
+
self.style.ERROR(f"❌ Failed to export CSV: {e}")
|
376
|
+
)
|