django-cfg 1.2.23__py3-none-any.whl → 1.2.25__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/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- django_cfg/apps/payments/config/__init__.py +15 -37
- django_cfg/apps/payments/config/module.py +30 -122
- django_cfg/apps/payments/config/providers.py +22 -0
- django_cfg/apps/payments/config/settings.py +53 -93
- django_cfg/apps/payments/config/utils.py +10 -156
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +33 -0
- django_cfg/apps/payments/migrations/0001_initial.py +94 -1
- django_cfg/apps/payments/models/payments.py +110 -0
- django_cfg/apps/payments/services/__init__.py +7 -1
- django_cfg/apps/payments/services/core/balance_service.py +14 -16
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +212 -29
- django_cfg/apps/payments/services/core/subscription_service.py +15 -17
- django_cfg/apps/payments/services/internal_types.py +31 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +14 -3
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
- django_cfg/apps/payments/signals/api_key_signals.py +10 -0
- django_cfg/apps/payments/signals/payment_signals.py +3 -2
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/utils/__init__.py +7 -4
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +2 -0
- django_cfg/apps/payments/views/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +11 -5
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/METADATA +10 -6
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/RECORD +77 -51
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- django_cfg/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,323 @@
|
|
1
|
+
"""
|
2
|
+
Management command to show currency database statistics.
|
3
|
+
|
4
|
+
Usage:
|
5
|
+
python manage.py currency_stats
|
6
|
+
python manage.py currency_stats --detailed
|
7
|
+
python manage.py currency_stats --top 10
|
8
|
+
python manage.py currency_stats --check-rates
|
9
|
+
"""
|
10
|
+
|
11
|
+
from datetime import datetime, timedelta
|
12
|
+
from typing import List
|
13
|
+
|
14
|
+
from django.core.management.base import BaseCommand
|
15
|
+
from django.utils import timezone
|
16
|
+
from django.db.models import Q, Count, Avg
|
17
|
+
|
18
|
+
from django_cfg.apps.payments.models.currencies import Currency
|
19
|
+
|
20
|
+
|
21
|
+
class Command(BaseCommand):
|
22
|
+
"""
|
23
|
+
Display currency database statistics and health information.
|
24
|
+
"""
|
25
|
+
|
26
|
+
help = 'Show currency database statistics'
|
27
|
+
|
28
|
+
def add_arguments(self, parser):
|
29
|
+
"""Add command line arguments."""
|
30
|
+
parser.add_argument(
|
31
|
+
'--detailed',
|
32
|
+
action='store_true',
|
33
|
+
help='Show detailed statistics'
|
34
|
+
)
|
35
|
+
|
36
|
+
parser.add_argument(
|
37
|
+
'--top',
|
38
|
+
type=int,
|
39
|
+
default=5,
|
40
|
+
help='Number of top currencies to show (default: 5)'
|
41
|
+
)
|
42
|
+
|
43
|
+
parser.add_argument(
|
44
|
+
'--check-rates',
|
45
|
+
action='store_true',
|
46
|
+
help='Check for outdated exchange rates'
|
47
|
+
)
|
48
|
+
|
49
|
+
parser.add_argument(
|
50
|
+
'--export-csv',
|
51
|
+
type=str,
|
52
|
+
help='Export currency data to CSV file'
|
53
|
+
)
|
54
|
+
|
55
|
+
def handle(self, *args, **options):
|
56
|
+
"""Main command handler."""
|
57
|
+
|
58
|
+
self.stdout.write(
|
59
|
+
self.style.SUCCESS('📊 Currency Database Statistics')
|
60
|
+
)
|
61
|
+
self.stdout.write('=' * 50)
|
62
|
+
|
63
|
+
self._show_basic_stats(options)
|
64
|
+
|
65
|
+
if options['detailed']:
|
66
|
+
self._show_detailed_stats(options)
|
67
|
+
|
68
|
+
if options['check_rates']:
|
69
|
+
self._check_rate_freshness()
|
70
|
+
|
71
|
+
if options['export_csv']:
|
72
|
+
self._export_to_csv(options['export_csv'])
|
73
|
+
|
74
|
+
def _show_basic_stats(self, options):
|
75
|
+
"""Show basic currency statistics."""
|
76
|
+
|
77
|
+
# Basic counts
|
78
|
+
total = Currency.objects.count()
|
79
|
+
active = Currency.objects.filter(is_active=True).count()
|
80
|
+
inactive = total - active
|
81
|
+
|
82
|
+
fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
|
83
|
+
crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
|
84
|
+
|
85
|
+
active_fiat = Currency.objects.filter(
|
86
|
+
currency_type=Currency.CurrencyType.FIAT,
|
87
|
+
is_active=True
|
88
|
+
).count()
|
89
|
+
active_crypto = Currency.objects.filter(
|
90
|
+
currency_type=Currency.CurrencyType.CRYPTO,
|
91
|
+
is_active=True
|
92
|
+
).count()
|
93
|
+
|
94
|
+
self.stdout.write(f"\n📈 Overview:")
|
95
|
+
self.stdout.write(f" Total currencies: {total}")
|
96
|
+
self.stdout.write(f" Active: {active} | Inactive: {inactive}")
|
97
|
+
self.stdout.write(f" Fiat: {fiat_count} ({active_fiat} active)")
|
98
|
+
self.stdout.write(f" Crypto: {crypto_count} ({active_crypto} active)")
|
99
|
+
|
100
|
+
# Rate update status
|
101
|
+
now = timezone.now()
|
102
|
+
|
103
|
+
# Recent (last 24h)
|
104
|
+
recent_threshold = now - timedelta(hours=24)
|
105
|
+
recent_updates = Currency.objects.filter(
|
106
|
+
rate_updated_at__gte=recent_threshold
|
107
|
+
).count()
|
108
|
+
|
109
|
+
# Outdated (older than 7 days)
|
110
|
+
outdated_threshold = now - timedelta(days=7)
|
111
|
+
outdated = Currency.objects.filter(
|
112
|
+
Q(rate_updated_at__lt=outdated_threshold) | Q(rate_updated_at__isnull=True)
|
113
|
+
).count()
|
114
|
+
|
115
|
+
self.stdout.write(f"\n🕒 Rate Updates:")
|
116
|
+
self.stdout.write(f" Updated in last 24h: {recent_updates}")
|
117
|
+
self.stdout.write(f" Outdated (>7 days): {outdated}")
|
118
|
+
|
119
|
+
# Top cryptocurrencies by USD value
|
120
|
+
top_crypto = Currency.objects.filter(
|
121
|
+
currency_type=Currency.CurrencyType.CRYPTO,
|
122
|
+
is_active=True
|
123
|
+
).order_by('-usd_rate')[:options['top']]
|
124
|
+
|
125
|
+
if top_crypto:
|
126
|
+
self.stdout.write(f"\n🚀 Top {options['top']} Cryptocurrencies by USD Rate:")
|
127
|
+
for i, currency in enumerate(top_crypto, 1):
|
128
|
+
age = self._get_rate_age(currency)
|
129
|
+
self.stdout.write(
|
130
|
+
f" {i}. {currency.code}: ${currency.usd_rate:,.6f} {age}"
|
131
|
+
)
|
132
|
+
|
133
|
+
# Major fiat currencies
|
134
|
+
major_fiat = Currency.objects.filter(
|
135
|
+
currency_type=Currency.CurrencyType.FIAT,
|
136
|
+
code__in=['USD', 'EUR', 'GBP', 'JPY', 'CNY'],
|
137
|
+
is_active=True
|
138
|
+
).order_by('code')
|
139
|
+
|
140
|
+
if major_fiat:
|
141
|
+
self.stdout.write(f"\n💵 Major Fiat Currencies:")
|
142
|
+
for currency in major_fiat:
|
143
|
+
age = self._get_rate_age(currency)
|
144
|
+
self.stdout.write(
|
145
|
+
f" • {currency.code}: {currency.name} = ${currency.usd_rate:.6f} {age}"
|
146
|
+
)
|
147
|
+
|
148
|
+
def _show_detailed_stats(self, options):
|
149
|
+
"""Show detailed statistics."""
|
150
|
+
|
151
|
+
self.stdout.write(f"\n📊 Detailed Statistics:")
|
152
|
+
|
153
|
+
# Decimal places distribution
|
154
|
+
decimal_stats = Currency.objects.values('decimal_places').annotate(
|
155
|
+
count=Count('decimal_places')
|
156
|
+
).order_by('decimal_places')
|
157
|
+
|
158
|
+
self.stdout.write(f"\n🔢 Decimal Places Distribution:")
|
159
|
+
for stat in decimal_stats:
|
160
|
+
self.stdout.write(f" {stat['decimal_places']} places: {stat['count']} currencies")
|
161
|
+
|
162
|
+
# Average rates by type
|
163
|
+
crypto_avg = Currency.objects.filter(
|
164
|
+
currency_type=Currency.CurrencyType.CRYPTO,
|
165
|
+
is_active=True
|
166
|
+
).aggregate(avg_rate=Avg('usd_rate'))['avg_rate']
|
167
|
+
|
168
|
+
fiat_avg = Currency.objects.filter(
|
169
|
+
currency_type=Currency.CurrencyType.FIAT,
|
170
|
+
is_active=True
|
171
|
+
).aggregate(avg_rate=Avg('usd_rate'))['avg_rate']
|
172
|
+
|
173
|
+
self.stdout.write(f"\n📊 Average USD Rates:")
|
174
|
+
if crypto_avg:
|
175
|
+
self.stdout.write(f" Cryptocurrencies: ${crypto_avg:.6f}")
|
176
|
+
if fiat_avg:
|
177
|
+
self.stdout.write(f" Fiat currencies: ${fiat_avg:.6f}")
|
178
|
+
|
179
|
+
# Min payment amounts
|
180
|
+
min_payment_stats = Currency.objects.values('min_payment_amount').annotate(
|
181
|
+
count=Count('min_payment_amount')
|
182
|
+
).order_by('min_payment_amount')[:5]
|
183
|
+
|
184
|
+
self.stdout.write(f"\n💰 Top Min Payment Amounts:")
|
185
|
+
for stat in min_payment_stats:
|
186
|
+
self.stdout.write(f" ${stat['min_payment_amount']}: {stat['count']} currencies")
|
187
|
+
|
188
|
+
# Rate freshness distribution
|
189
|
+
now = timezone.now()
|
190
|
+
thresholds = [
|
191
|
+
('Last hour', timedelta(hours=1)),
|
192
|
+
('Last 24 hours', timedelta(hours=24)),
|
193
|
+
('Last week', timedelta(days=7)),
|
194
|
+
('Last month', timedelta(days=30)),
|
195
|
+
]
|
196
|
+
|
197
|
+
self.stdout.write(f"\n⏰ Rate Update Distribution:")
|
198
|
+
previous_count = 0
|
199
|
+
for label, delta in thresholds:
|
200
|
+
threshold = now - delta
|
201
|
+
count = Currency.objects.filter(rate_updated_at__gte=threshold).count()
|
202
|
+
new_in_period = count - previous_count
|
203
|
+
self.stdout.write(f" {label}: {new_in_period} new updates ({count} total)")
|
204
|
+
previous_count = count
|
205
|
+
|
206
|
+
# Never updated
|
207
|
+
never_updated = Currency.objects.filter(rate_updated_at__isnull=True).count()
|
208
|
+
if never_updated > 0:
|
209
|
+
self.stdout.write(f" Never updated: {never_updated} currencies")
|
210
|
+
|
211
|
+
def _check_rate_freshness(self):
|
212
|
+
"""Check for outdated exchange rates."""
|
213
|
+
|
214
|
+
self.stdout.write(f"\n🔍 Rate Freshness Check:")
|
215
|
+
|
216
|
+
now = timezone.now()
|
217
|
+
|
218
|
+
# Very outdated (>30 days)
|
219
|
+
very_old_threshold = now - timedelta(days=30)
|
220
|
+
very_old = Currency.objects.filter(
|
221
|
+
Q(rate_updated_at__lt=very_old_threshold) | Q(rate_updated_at__isnull=True),
|
222
|
+
is_active=True
|
223
|
+
)
|
224
|
+
|
225
|
+
if very_old.exists():
|
226
|
+
self.stdout.write(
|
227
|
+
self.style.ERROR(f" ❌ {very_old.count()} currencies with very old rates (>30 days)")
|
228
|
+
)
|
229
|
+
for currency in very_old[:5]:
|
230
|
+
age = self._get_rate_age(currency)
|
231
|
+
self.stdout.write(f" • {currency.code}: {age}")
|
232
|
+
if very_old.count() > 5:
|
233
|
+
self.stdout.write(f" ... and {very_old.count() - 5} more")
|
234
|
+
|
235
|
+
# Moderately outdated (7-30 days)
|
236
|
+
old_threshold = now - timedelta(days=7)
|
237
|
+
old_currencies = Currency.objects.filter(
|
238
|
+
rate_updated_at__lt=old_threshold,
|
239
|
+
rate_updated_at__gte=very_old_threshold,
|
240
|
+
is_active=True
|
241
|
+
)
|
242
|
+
|
243
|
+
if old_currencies.exists():
|
244
|
+
self.stdout.write(
|
245
|
+
self.style.WARNING(f" ⚠️ {old_currencies.count()} currencies with old rates (7-30 days)")
|
246
|
+
)
|
247
|
+
|
248
|
+
# Fresh rates (last 24h)
|
249
|
+
fresh_threshold = now - timedelta(hours=24)
|
250
|
+
fresh = Currency.objects.filter(
|
251
|
+
rate_updated_at__gte=fresh_threshold,
|
252
|
+
is_active=True
|
253
|
+
).count()
|
254
|
+
|
255
|
+
if fresh > 0:
|
256
|
+
self.stdout.write(
|
257
|
+
self.style.SUCCESS(f" ✅ {fresh} currencies with fresh rates (<24h)")
|
258
|
+
)
|
259
|
+
|
260
|
+
# Recommendations
|
261
|
+
total_active = Currency.objects.filter(is_active=True).count()
|
262
|
+
if very_old.count() > 0:
|
263
|
+
self.stdout.write(f"\n💡 Recommendations:")
|
264
|
+
self.stdout.write(f" • Run: python manage.py update_currencies --force-update")
|
265
|
+
self.stdout.write(f" • Consider deactivating currencies with very old rates")
|
266
|
+
|
267
|
+
def _get_rate_age(self, currency) -> str:
|
268
|
+
"""Get human-readable age of currency rate."""
|
269
|
+
if not currency.rate_updated_at:
|
270
|
+
return "(never updated)"
|
271
|
+
|
272
|
+
age = timezone.now() - currency.rate_updated_at
|
273
|
+
|
274
|
+
if age.days > 30:
|
275
|
+
return f"({age.days} days ago)"
|
276
|
+
elif age.days > 0:
|
277
|
+
return f"({age.days}d ago)"
|
278
|
+
elif age.seconds > 3600:
|
279
|
+
hours = age.seconds // 3600
|
280
|
+
return f"({hours}h ago)"
|
281
|
+
else:
|
282
|
+
minutes = age.seconds // 60
|
283
|
+
return f"({minutes}m ago)"
|
284
|
+
|
285
|
+
def _export_to_csv(self, filename: str):
|
286
|
+
"""Export currency data to CSV file."""
|
287
|
+
import csv
|
288
|
+
|
289
|
+
self.stdout.write(f"\n📁 Exporting to {filename}...")
|
290
|
+
|
291
|
+
try:
|
292
|
+
with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
|
293
|
+
writer = csv.writer(csvfile)
|
294
|
+
|
295
|
+
# Header
|
296
|
+
writer.writerow([
|
297
|
+
'code', 'name', 'symbol', 'currency_type', 'decimal_places',
|
298
|
+
'usd_rate', 'min_payment_amount', 'is_active', 'rate_updated_at'
|
299
|
+
])
|
300
|
+
|
301
|
+
# Data
|
302
|
+
currencies = Currency.objects.all().order_by('code')
|
303
|
+
for currency in currencies:
|
304
|
+
writer.writerow([
|
305
|
+
currency.code,
|
306
|
+
currency.name,
|
307
|
+
currency.symbol,
|
308
|
+
currency.currency_type,
|
309
|
+
currency.decimal_places,
|
310
|
+
currency.usd_rate,
|
311
|
+
currency.min_payment_amount,
|
312
|
+
currency.is_active,
|
313
|
+
currency.rate_updated_at.isoformat() if currency.rate_updated_at else None
|
314
|
+
])
|
315
|
+
|
316
|
+
self.stdout.write(
|
317
|
+
self.style.SUCCESS(f" ✅ Exported {currencies.count()} currencies to {filename}")
|
318
|
+
)
|
319
|
+
|
320
|
+
except Exception as e:
|
321
|
+
self.stdout.write(
|
322
|
+
self.style.ERROR(f" ❌ Export failed: {str(e)}")
|
323
|
+
)
|
@@ -0,0 +1,246 @@
|
|
1
|
+
"""
|
2
|
+
Management command to populate initial currency data.
|
3
|
+
|
4
|
+
This is a simpler version of update_currencies designed for initial setup.
|
5
|
+
Use this when you need to populate an empty currency database.
|
6
|
+
|
7
|
+
Usage:
|
8
|
+
python manage.py populate_currencies
|
9
|
+
python manage.py populate_currencies --quick
|
10
|
+
python manage.py populate_currencies --crypto-only
|
11
|
+
python manage.py populate_currencies --fiat-only
|
12
|
+
"""
|
13
|
+
|
14
|
+
import logging
|
15
|
+
from typing import List
|
16
|
+
|
17
|
+
from django.core.management.base import BaseCommand, CommandError
|
18
|
+
from django.db import transaction
|
19
|
+
from django.utils import timezone
|
20
|
+
|
21
|
+
from django_cfg.modules.django_currency.database.database_loader import (
|
22
|
+
create_database_loader,
|
23
|
+
DatabaseLoaderConfig
|
24
|
+
)
|
25
|
+
from django_cfg.apps.payments.models.currencies import Currency
|
26
|
+
|
27
|
+
logger = logging.getLogger(__name__)
|
28
|
+
|
29
|
+
|
30
|
+
class Command(BaseCommand):
|
31
|
+
"""
|
32
|
+
Simple command to populate initial currency data.
|
33
|
+
|
34
|
+
Optimized for first-time setup with sensible defaults.
|
35
|
+
"""
|
36
|
+
|
37
|
+
help = 'Populate initial currency data (for empty database)'
|
38
|
+
|
39
|
+
def add_arguments(self, parser):
|
40
|
+
"""Add command line arguments."""
|
41
|
+
parser.add_argument(
|
42
|
+
'--quick',
|
43
|
+
action='store_true',
|
44
|
+
help='Quick setup with top 50 cryptocurrencies and 20 fiat currencies'
|
45
|
+
)
|
46
|
+
|
47
|
+
parser.add_argument(
|
48
|
+
'--crypto-only',
|
49
|
+
action='store_true',
|
50
|
+
help='Only populate cryptocurrencies'
|
51
|
+
)
|
52
|
+
|
53
|
+
parser.add_argument(
|
54
|
+
'--fiat-only',
|
55
|
+
action='store_true',
|
56
|
+
help='Only populate fiat currencies'
|
57
|
+
)
|
58
|
+
|
59
|
+
parser.add_argument(
|
60
|
+
'--skip-existing',
|
61
|
+
action='store_true',
|
62
|
+
help='Skip currencies that already exist in database'
|
63
|
+
)
|
64
|
+
|
65
|
+
def handle(self, *args, **options):
|
66
|
+
"""Main command handler."""
|
67
|
+
|
68
|
+
self.stdout.write(
|
69
|
+
self.style.SUCCESS('🪙 Populating currency database...')
|
70
|
+
)
|
71
|
+
|
72
|
+
# Check if database is empty
|
73
|
+
existing_count = Currency.objects.count()
|
74
|
+
if existing_count > 0 and not options['skip_existing']:
|
75
|
+
self.stdout.write(
|
76
|
+
self.style.WARNING(f'⚠️ Database already contains {existing_count} currencies')
|
77
|
+
)
|
78
|
+
response = input('Continue anyway? [y/N]: ')
|
79
|
+
if response.lower() != 'y':
|
80
|
+
self.stdout.write('Cancelled by user')
|
81
|
+
return
|
82
|
+
|
83
|
+
try:
|
84
|
+
# Configure loader based on options
|
85
|
+
if options['quick']:
|
86
|
+
config = DatabaseLoaderConfig(
|
87
|
+
max_cryptocurrencies=50,
|
88
|
+
max_fiat_currencies=20,
|
89
|
+
min_market_cap_usd=10_000_000, # Top coins only
|
90
|
+
coingecko_delay=1.0, # Faster for initial setup
|
91
|
+
)
|
92
|
+
self.stdout.write("⚡ Quick setup mode: top 50 crypto + 20 fiat")
|
93
|
+
else:
|
94
|
+
config = DatabaseLoaderConfig(
|
95
|
+
max_cryptocurrencies=200,
|
96
|
+
max_fiat_currencies=30,
|
97
|
+
min_market_cap_usd=1_000_000,
|
98
|
+
coingecko_delay=1.5,
|
99
|
+
)
|
100
|
+
self.stdout.write("📈 Standard setup: top 200 crypto + 30 fiat")
|
101
|
+
|
102
|
+
loader = create_database_loader(
|
103
|
+
max_cryptocurrencies=config.max_cryptocurrencies,
|
104
|
+
max_fiat_currencies=config.max_fiat_currencies,
|
105
|
+
min_market_cap_usd=config.min_market_cap_usd,
|
106
|
+
coingecko_delay=config.coingecko_delay
|
107
|
+
)
|
108
|
+
|
109
|
+
# Get statistics
|
110
|
+
stats = loader.get_statistics()
|
111
|
+
self.stdout.write(f"📊 Available: {stats['total_currencies']} currencies")
|
112
|
+
|
113
|
+
# Load currency data
|
114
|
+
self.stdout.write("🌐 Fetching currency data from APIs...")
|
115
|
+
fresh_currencies = loader.build_currency_database_data()
|
116
|
+
|
117
|
+
# Filter by type if requested
|
118
|
+
if options['crypto_only']:
|
119
|
+
fresh_currencies = [c for c in fresh_currencies if c.currency_type == 'crypto']
|
120
|
+
self.stdout.write(f"🔗 Crypto only: {len(fresh_currencies)} cryptocurrencies")
|
121
|
+
elif options['fiat_only']:
|
122
|
+
fresh_currencies = [c for c in fresh_currencies if c.currency_type == 'fiat']
|
123
|
+
self.stdout.write(f"💵 Fiat only: {len(fresh_currencies)} fiat currencies")
|
124
|
+
else:
|
125
|
+
crypto_count = sum(1 for c in fresh_currencies if c.currency_type == 'crypto')
|
126
|
+
fiat_count = sum(1 for c in fresh_currencies if c.currency_type == 'fiat')
|
127
|
+
self.stdout.write(f"💰 Mixed: {crypto_count} crypto + {fiat_count} fiat")
|
128
|
+
|
129
|
+
# Populate database
|
130
|
+
self._populate_database(fresh_currencies, options)
|
131
|
+
|
132
|
+
except KeyboardInterrupt:
|
133
|
+
self.stdout.write(
|
134
|
+
self.style.WARNING('\n⚠️ Population interrupted by user')
|
135
|
+
)
|
136
|
+
raise CommandError("Population cancelled")
|
137
|
+
|
138
|
+
except Exception as e:
|
139
|
+
logger.exception("Currency population failed")
|
140
|
+
self.stdout.write(
|
141
|
+
self.style.ERROR(f'❌ Population failed: {str(e)}')
|
142
|
+
)
|
143
|
+
raise CommandError(f"Population failed: {str(e)}")
|
144
|
+
|
145
|
+
def _populate_database(self, currencies: List, options: dict):
|
146
|
+
"""Populate the database with currencies."""
|
147
|
+
|
148
|
+
created_count = 0
|
149
|
+
updated_count = 0
|
150
|
+
skipped_count = 0
|
151
|
+
|
152
|
+
try:
|
153
|
+
with transaction.atomic():
|
154
|
+
self.stdout.write("💾 Populating database...")
|
155
|
+
|
156
|
+
for i, currency_data in enumerate(currencies):
|
157
|
+
try:
|
158
|
+
# Check if exists and should skip
|
159
|
+
if options['skip_existing']:
|
160
|
+
if Currency.objects.filter(code=currency_data.code).exists():
|
161
|
+
skipped_count += 1
|
162
|
+
continue
|
163
|
+
|
164
|
+
# Create or update
|
165
|
+
currency, created = Currency.objects.update_or_create(
|
166
|
+
code=currency_data.code,
|
167
|
+
defaults={
|
168
|
+
'name': currency_data.name,
|
169
|
+
'symbol': currency_data.symbol,
|
170
|
+
'currency_type': currency_data.currency_type,
|
171
|
+
'decimal_places': currency_data.decimal_places,
|
172
|
+
'usd_rate': currency_data.usd_rate,
|
173
|
+
'min_payment_amount': currency_data.min_payment_amount,
|
174
|
+
'is_active': currency_data.is_active,
|
175
|
+
'rate_updated_at': timezone.now()
|
176
|
+
}
|
177
|
+
)
|
178
|
+
|
179
|
+
if created:
|
180
|
+
created_count += 1
|
181
|
+
else:
|
182
|
+
updated_count += 1
|
183
|
+
|
184
|
+
# Progress indicator every 25 currencies
|
185
|
+
if (i + 1) % 25 == 0:
|
186
|
+
self.stdout.write(f" 📊 Progress: {i + 1}/{len(currencies)}")
|
187
|
+
|
188
|
+
except Exception as e:
|
189
|
+
self.stdout.write(
|
190
|
+
self.style.WARNING(f'⚠️ Failed to create {currency_data.code}: {e}')
|
191
|
+
)
|
192
|
+
continue
|
193
|
+
|
194
|
+
# Final summary
|
195
|
+
total_processed = created_count + updated_count
|
196
|
+
self.stdout.write(f"\n🎉 Population completed!")
|
197
|
+
self.stdout.write(f" ✅ Created: {created_count} currencies")
|
198
|
+
self.stdout.write(f" 🔄 Updated: {updated_count} currencies")
|
199
|
+
self.stdout.write(f" ⏭️ Skipped: {skipped_count} currencies")
|
200
|
+
self.stdout.write(f" 📊 Total: {total_processed} currencies processed")
|
201
|
+
|
202
|
+
# Show some examples
|
203
|
+
self._show_examples()
|
204
|
+
|
205
|
+
self.stdout.write(
|
206
|
+
self.style.SUCCESS('\n✅ Currency database is ready for payments!')
|
207
|
+
)
|
208
|
+
|
209
|
+
except Exception as e:
|
210
|
+
self.stdout.write(
|
211
|
+
self.style.ERROR(f'❌ Population failed and rolled back: {str(e)}')
|
212
|
+
)
|
213
|
+
raise
|
214
|
+
|
215
|
+
def _show_examples(self):
|
216
|
+
"""Show some example currencies that were created."""
|
217
|
+
|
218
|
+
# Show top cryptocurrencies
|
219
|
+
crypto_examples = Currency.objects.filter(
|
220
|
+
currency_type=Currency.CurrencyType.CRYPTO
|
221
|
+
).order_by('-usd_rate')[:3]
|
222
|
+
|
223
|
+
if crypto_examples:
|
224
|
+
self.stdout.write(f"\n🔗 Top cryptocurrencies:")
|
225
|
+
for currency in crypto_examples:
|
226
|
+
self.stdout.write(f" • {currency.code}: ${currency.usd_rate:.2f}")
|
227
|
+
|
228
|
+
# Show fiat currencies
|
229
|
+
fiat_examples = Currency.objects.filter(
|
230
|
+
currency_type=Currency.CurrencyType.FIAT
|
231
|
+
).order_by('code')[:5]
|
232
|
+
|
233
|
+
if fiat_examples:
|
234
|
+
self.stdout.write(f"\n💵 Fiat currencies:")
|
235
|
+
for currency in fiat_examples:
|
236
|
+
self.stdout.write(f" • {currency.code}: {currency.name} ({currency.symbol})")
|
237
|
+
|
238
|
+
# Total counts
|
239
|
+
total = Currency.objects.count()
|
240
|
+
crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
|
241
|
+
fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
|
242
|
+
|
243
|
+
self.stdout.write(f"\n📊 Database now contains:")
|
244
|
+
self.stdout.write(f" • Total: {total} currencies")
|
245
|
+
self.stdout.write(f" • Crypto: {crypto_count} currencies")
|
246
|
+
self.stdout.write(f" • Fiat: {fiat_count} currencies")
|