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.
Files changed (85) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
  3. django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
  4. django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
  5. django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
  6. django_cfg/apps/payments/config/__init__.py +15 -37
  7. django_cfg/apps/payments/config/module.py +30 -122
  8. django_cfg/apps/payments/config/providers.py +22 -0
  9. django_cfg/apps/payments/config/settings.py +53 -93
  10. django_cfg/apps/payments/config/utils.py +10 -156
  11. django_cfg/apps/payments/management/__init__.py +3 -0
  12. django_cfg/apps/payments/management/commands/README.md +178 -0
  13. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  14. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  15. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  16. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  17. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  18. django_cfg/apps/payments/middleware/api_access.py +33 -0
  19. django_cfg/apps/payments/migrations/0001_initial.py +94 -1
  20. django_cfg/apps/payments/models/payments.py +110 -0
  21. django_cfg/apps/payments/services/__init__.py +7 -1
  22. django_cfg/apps/payments/services/core/balance_service.py +14 -16
  23. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  24. django_cfg/apps/payments/services/core/payment_service.py +212 -29
  25. django_cfg/apps/payments/services/core/subscription_service.py +15 -17
  26. django_cfg/apps/payments/services/internal_types.py +31 -0
  27. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  28. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  29. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  30. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  31. django_cfg/apps/payments/services/providers/cryptapi.py +14 -3
  32. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  33. django_cfg/apps/payments/services/providers/registry.py +4 -0
  34. django_cfg/apps/payments/services/security/__init__.py +34 -0
  35. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  36. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  37. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  38. django_cfg/apps/payments/signals/api_key_signals.py +10 -0
  39. django_cfg/apps/payments/signals/payment_signals.py +3 -2
  40. django_cfg/apps/payments/tasks/__init__.py +12 -0
  41. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  42. django_cfg/apps/payments/utils/__init__.py +7 -4
  43. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  44. django_cfg/apps/payments/utils/config_utils.py +2 -0
  45. django_cfg/apps/payments/views/payment_views.py +40 -2
  46. django_cfg/apps/payments/views/webhook_views.py +266 -0
  47. django_cfg/apps/payments/viewsets.py +65 -0
  48. django_cfg/cli/README.md +2 -2
  49. django_cfg/cli/commands/create_project.py +1 -1
  50. django_cfg/cli/commands/info.py +1 -1
  51. django_cfg/cli/main.py +1 -1
  52. django_cfg/cli/utils.py +5 -5
  53. django_cfg/core/config.py +18 -4
  54. django_cfg/models/payments.py +546 -0
  55. django_cfg/models/tasks.py +51 -2
  56. django_cfg/modules/base.py +11 -5
  57. django_cfg/modules/django_currency/README.md +104 -269
  58. django_cfg/modules/django_currency/__init__.py +99 -41
  59. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  60. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  61. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  62. django_cfg/modules/django_currency/core/__init__.py +42 -0
  63. django_cfg/modules/django_currency/core/converter.py +169 -0
  64. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  65. django_cfg/modules/django_currency/core/models.py +54 -0
  66. django_cfg/modules/django_currency/database/__init__.py +25 -0
  67. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  68. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  69. django_cfg/modules/django_currency/utils/cache.py +92 -0
  70. django_cfg/registry/core.py +10 -0
  71. django_cfg/template_archive/__init__.py +0 -0
  72. django_cfg/template_archive/django_sample.zip +0 -0
  73. {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/METADATA +10 -6
  74. {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/RECORD +77 -51
  75. django_cfg/apps/agents/examples/__init__.py +0 -3
  76. django_cfg/apps/agents/examples/simple_example.py +0 -161
  77. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  78. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  79. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  80. django_cfg/modules/django_currency/cache.py +0 -430
  81. django_cfg/modules/django_currency/converter.py +0 -324
  82. django_cfg/modules/django_currency/service.py +0 -277
  83. {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  84. {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  85. {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")