django-cfg 1.2.27__py3-none-any.whl → 1.2.31__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 (138) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin/__init__.py +3 -2
  3. django_cfg/apps/payments/admin/balance_admin.py +18 -18
  4. django_cfg/apps/payments/admin/currencies_admin.py +319 -131
  5. django_cfg/apps/payments/admin/payments_admin.py +15 -4
  6. django_cfg/apps/payments/config/module.py +2 -2
  7. django_cfg/apps/payments/config/utils.py +2 -2
  8. django_cfg/apps/payments/decorators.py +2 -2
  9. django_cfg/apps/payments/management/commands/README.md +95 -127
  10. django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
  11. django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
  12. django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
  13. django_cfg/apps/payments/managers/__init__.py +3 -2
  14. django_cfg/apps/payments/managers/balance_manager.py +2 -2
  15. django_cfg/apps/payments/managers/currency_manager.py +272 -49
  16. django_cfg/apps/payments/managers/payment_manager.py +161 -13
  17. django_cfg/apps/payments/middleware/api_access.py +2 -2
  18. django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
  19. django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
  20. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
  21. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
  22. django_cfg/apps/payments/models/__init__.py +3 -2
  23. django_cfg/apps/payments/models/currencies.py +187 -71
  24. django_cfg/apps/payments/models/payments.py +3 -2
  25. django_cfg/apps/payments/serializers/__init__.py +3 -2
  26. django_cfg/apps/payments/serializers/currencies.py +20 -12
  27. django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
  28. django_cfg/apps/payments/services/core/balance_service.py +2 -2
  29. django_cfg/apps/payments/services/core/fallback_service.py +2 -2
  30. django_cfg/apps/payments/services/core/payment_service.py +3 -6
  31. django_cfg/apps/payments/services/core/subscription_service.py +4 -7
  32. django_cfg/apps/payments/services/internal_types.py +171 -7
  33. django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
  34. django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
  35. django_cfg/apps/payments/services/providers/base.py +144 -43
  36. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
  37. django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
  38. django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
  39. django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
  40. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
  41. django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
  42. django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
  43. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
  44. django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
  45. django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
  46. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
  47. django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
  48. django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
  49. django_cfg/apps/payments/services/providers/registry.py +294 -11
  50. django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
  51. django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
  52. django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
  53. django_cfg/apps/payments/services/security/error_handler.py +6 -8
  54. django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
  55. django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
  56. django_cfg/apps/payments/signals/api_key_signals.py +2 -2
  57. django_cfg/apps/payments/signals/payment_signals.py +11 -5
  58. django_cfg/apps/payments/signals/subscription_signals.py +2 -2
  59. django_cfg/apps/payments/static/payments/css/payments.css +340 -0
  60. django_cfg/apps/payments/static/payments/js/notifications.js +202 -0
  61. django_cfg/apps/payments/static/payments/js/payment-utils.js +318 -0
  62. django_cfg/apps/payments/static/payments/js/theme.js +86 -0
  63. django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
  64. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
  65. django_cfg/apps/payments/templates/payments/base.html +182 -0
  66. django_cfg/apps/payments/templates/payments/components/payment_card.html +201 -0
  67. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +109 -0
  68. django_cfg/apps/payments/templates/payments/components/progress_bar.html +43 -0
  69. django_cfg/apps/payments/templates/payments/components/provider_stats.html +40 -0
  70. django_cfg/apps/payments/templates/payments/components/status_badge.html +34 -0
  71. django_cfg/apps/payments/templates/payments/components/status_overview.html +148 -0
  72. django_cfg/apps/payments/templates/payments/dashboard.html +258 -0
  73. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
  74. django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
  75. django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
  76. django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
  77. django_cfg/apps/payments/templates/payments/stats.html +261 -0
  78. django_cfg/apps/payments/templates/payments/test.html +213 -0
  79. django_cfg/apps/payments/templatetags/__init__.py +1 -0
  80. django_cfg/apps/payments/templatetags/payments_tags.py +315 -0
  81. django_cfg/apps/payments/urls.py +3 -1
  82. django_cfg/apps/payments/urls_admin.py +58 -0
  83. django_cfg/apps/payments/utils/__init__.py +1 -3
  84. django_cfg/apps/payments/utils/billing_utils.py +2 -2
  85. django_cfg/apps/payments/utils/config_utils.py +2 -8
  86. django_cfg/apps/payments/utils/validation_utils.py +2 -2
  87. django_cfg/apps/payments/views/__init__.py +3 -2
  88. django_cfg/apps/payments/views/currency_views.py +31 -20
  89. django_cfg/apps/payments/views/payment_views.py +2 -2
  90. django_cfg/apps/payments/views/templates/__init__.py +25 -0
  91. django_cfg/apps/payments/views/templates/ajax.py +451 -0
  92. django_cfg/apps/payments/views/templates/base.py +212 -0
  93. django_cfg/apps/payments/views/templates/dashboard.py +60 -0
  94. django_cfg/apps/payments/views/templates/payment_detail.py +102 -0
  95. django_cfg/apps/payments/views/templates/payment_management.py +158 -0
  96. django_cfg/apps/payments/views/templates/qr_code.py +174 -0
  97. django_cfg/apps/payments/views/templates/stats.py +244 -0
  98. django_cfg/apps/payments/views/templates/utils.py +181 -0
  99. django_cfg/apps/payments/views/webhook_views.py +2 -2
  100. django_cfg/apps/payments/viewsets.py +3 -2
  101. django_cfg/apps/tasks/urls.py +0 -2
  102. django_cfg/apps/tasks/urls_admin.py +14 -0
  103. django_cfg/apps/urls.py +6 -3
  104. django_cfg/core/config.py +35 -0
  105. django_cfg/models/payments.py +2 -8
  106. django_cfg/modules/django_currency/__init__.py +16 -11
  107. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  108. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  109. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  110. django_cfg/modules/django_currency/core/__init__.py +1 -7
  111. django_cfg/modules/django_currency/core/converter.py +18 -23
  112. django_cfg/modules/django_currency/core/models.py +122 -11
  113. django_cfg/modules/django_currency/database/__init__.py +4 -4
  114. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  115. django_cfg/modules/django_unfold/dashboard.py +7 -2
  116. django_cfg/registry/core.py +1 -0
  117. django_cfg/template_archive/.gitignore +1 -0
  118. django_cfg/template_archive/django_sample.zip +0 -0
  119. django_cfg/templates/admin/components/action_grid.html +9 -9
  120. django_cfg/templates/admin/components/metric_card.html +5 -5
  121. django_cfg/templates/admin/components/status_badge.html +2 -2
  122. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  123. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  124. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  125. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  126. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/METADATA +13 -18
  127. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/RECORD +130 -83
  128. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  129. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  130. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  131. django_cfg/apps/payments/services/providers/cryptomus.py +0 -310
  132. django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
  133. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  134. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  135. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  136. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
  137. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
  138. {django_cfg-1.2.27.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -1,246 +0,0 @@
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")
@@ -1,336 +0,0 @@
1
- """
2
- Management command to update currency data using django_currency database loader.
3
-
4
- This command automatically populates and updates the payments Currency model
5
- with fresh data from external APIs (CoinGecko, YFinance).
6
-
7
- Usage:
8
- python manage.py update_currencies
9
- python manage.py update_currencies --max-crypto 100 --max-fiat 20
10
- python manage.py update_currencies --dry-run
11
- python manage.py update_currencies --force-update
12
- """
13
-
14
- import logging
15
- from typing import Dict, List, Optional
16
- from datetime import datetime, timedelta
17
-
18
- from django.core.management.base import BaseCommand, CommandError
19
- from django.db import transaction
20
- from django.utils import timezone
21
- from django.conf import settings
22
-
23
- from django_cfg.modules.django_currency.database.database_loader import (
24
- CurrencyDatabaseLoader,
25
- DatabaseLoaderConfig,
26
- create_database_loader
27
- )
28
- from django_cfg.apps.payments.models.currencies import Currency
29
-
30
- logger = logging.getLogger(__name__)
31
-
32
-
33
- class Command(BaseCommand):
34
- """
35
- Management command to update currency data.
36
-
37
- Features:
38
- - Automatic detection of new currencies
39
- - Rate updates for existing currencies
40
- - Dry-run mode for testing
41
- - Configurable limits for API calls
42
- - Progress reporting
43
- - Error handling and rollback
44
- """
45
-
46
- help = 'Update currency data from external APIs (CoinGecko, YFinance)'
47
-
48
- def add_arguments(self, parser):
49
- """Add command line arguments."""
50
- parser.add_argument(
51
- '--max-crypto',
52
- type=int,
53
- default=500,
54
- help='Maximum number of cryptocurrencies to load (default: 500)'
55
- )
56
-
57
- parser.add_argument(
58
- '--max-fiat',
59
- type=int,
60
- default=50,
61
- help='Maximum number of fiat currencies to load (default: 50)'
62
- )
63
-
64
- parser.add_argument(
65
- '--min-market-cap',
66
- type=float,
67
- default=1000000,
68
- help='Minimum market cap in USD for cryptocurrencies (default: 1M)'
69
- )
70
-
71
- parser.add_argument(
72
- '--dry-run',
73
- action='store_true',
74
- help='Show what would be updated without making changes'
75
- )
76
-
77
- parser.add_argument(
78
- '--force-update',
79
- action='store_true',
80
- help='Force update all currencies even if recently updated'
81
- )
82
-
83
- parser.add_argument(
84
- '--exclude-stablecoins',
85
- action='store_true',
86
- help='Exclude stablecoins from cryptocurrency updates'
87
- )
88
-
89
- parser.add_argument(
90
- '--update-threshold-hours',
91
- type=int,
92
- default=6,
93
- help='Only update currencies older than N hours (default: 6)'
94
- )
95
-
96
- parser.add_argument(
97
- '--verbose',
98
- action='store_true',
99
- help='Verbose output with detailed progress'
100
- )
101
-
102
- def handle(self, *args, **options):
103
- """Main command handler."""
104
-
105
- # Configure logging
106
- log_level = logging.INFO if options['verbose'] else logging.WARNING
107
- logging.getLogger('django_cfg.modules.django_currency').setLevel(log_level)
108
-
109
- self.stdout.write(
110
- self.style.SUCCESS('šŸ¦ Starting currency database update...')
111
- )
112
-
113
- try:
114
- # Create database loader with options
115
- config = DatabaseLoaderConfig(
116
- max_cryptocurrencies=options['max_crypto'],
117
- max_fiat_currencies=options['max_fiat'],
118
- min_market_cap_usd=options['min_market_cap'],
119
- exclude_stablecoins=options['exclude_stablecoins'],
120
- coingecko_delay=1.5, # Be respectful to APIs
121
- yfinance_delay=0.5
122
- )
123
-
124
- loader = CurrencyDatabaseLoader(config)
125
-
126
- # Get statistics
127
- stats = loader.get_statistics()
128
- self.stdout.write(f"šŸ“Š Loader config: {stats['total_currencies']} currencies available")
129
- self.stdout.write(f" • {stats['total_fiat_currencies']} fiat currencies")
130
- self.stdout.write(f" • {stats['total_cryptocurrencies']} cryptocurrencies")
131
-
132
- # Check existing currencies
133
- existing_count = Currency.objects.count()
134
- self.stdout.write(f"šŸ“‹ Current database: {existing_count} currencies")
135
-
136
- # Determine update strategy
137
- if options['force_update']:
138
- currencies_to_update = Currency.objects.all()
139
- self.stdout.write("šŸ”„ Force update mode: updating all currencies")
140
- else:
141
- threshold = timezone.now() - timedelta(hours=options['update_threshold_hours'])
142
- currencies_to_update = Currency.objects.filter(
143
- rate_updated_at__lt=threshold
144
- ) | Currency.objects.filter(rate_updated_at__isnull=True)
145
-
146
- self.stdout.write(
147
- f"ā° Updating currencies older than {options['update_threshold_hours']} hours: "
148
- f"{currencies_to_update.count()} currencies"
149
- )
150
-
151
- # Load fresh currency data
152
- self.stdout.write("🌐 Fetching fresh currency data from APIs...")
153
- fresh_currencies = loader.build_currency_database_data()
154
-
155
- if options['dry_run']:
156
- self._handle_dry_run(fresh_currencies, currencies_to_update)
157
- else:
158
- self._handle_update(fresh_currencies, currencies_to_update, options)
159
-
160
- except KeyboardInterrupt:
161
- self.stdout.write(
162
- self.style.WARNING('\nāš ļø Update interrupted by user')
163
- )
164
- raise CommandError("Update cancelled by user")
165
-
166
- except Exception as e:
167
- logger.exception("Currency update failed")
168
- self.stdout.write(
169
- self.style.ERROR(f'āŒ Currency update failed: {str(e)}')
170
- )
171
- raise CommandError(f"Update failed: {str(e)}")
172
-
173
- def _handle_dry_run(self, fresh_currencies: List, currencies_to_update):
174
- """Handle dry-run mode - show what would be updated."""
175
- self.stdout.write(
176
- self.style.WARNING('🧪 DRY RUN MODE - No changes will be made')
177
- )
178
-
179
- # Analyze changes
180
- existing_codes = set(Currency.objects.values_list('code', flat=True))
181
- fresh_codes = {curr.code for curr in fresh_currencies}
182
-
183
- new_currencies = fresh_codes - existing_codes
184
- existing_currencies = fresh_codes & existing_codes
185
-
186
- self.stdout.write(f"\nšŸ“ˆ Analysis:")
187
- self.stdout.write(f" • Would add {len(new_currencies)} new currencies")
188
- self.stdout.write(f" • Would update {len(existing_currencies)} existing currencies")
189
-
190
- if new_currencies:
191
- self.stdout.write(f"\nāž• New currencies to add:")
192
- for code in sorted(list(new_currencies)[:10]): # Show first 10
193
- currency = next(c for c in fresh_currencies if c.code == code)
194
- self.stdout.write(f" • {code}: {currency.name} ({currency.currency_type})")
195
- if len(new_currencies) > 10:
196
- self.stdout.write(f" ... and {len(new_currencies) - 10} more")
197
-
198
- if existing_currencies:
199
- self.stdout.write(f"\nšŸ”„ Existing currencies to update:")
200
- for code in sorted(list(existing_currencies)[:10]): # Show first 10
201
- currency = next(c for c in fresh_currencies if c.code == code)
202
- try:
203
- existing = Currency.objects.get(code=code)
204
- rate_diff = abs(existing.usd_rate - currency.usd_rate)
205
- if rate_diff > 0.01: # Significant change
206
- change_pct = ((currency.usd_rate - existing.usd_rate) / existing.usd_rate) * 100
207
- self.stdout.write(
208
- f" • {code}: ${existing.usd_rate:.6f} → ${currency.usd_rate:.6f} "
209
- f"({change_pct:+.2f}%)"
210
- )
211
- except Currency.DoesNotExist:
212
- pass
213
-
214
- self.stdout.write(
215
- self.style.SUCCESS('\nāœ… Dry run completed - use --force-update to apply changes')
216
- )
217
-
218
- def _handle_update(self, fresh_currencies: List, currencies_to_update, options: Dict):
219
- """Handle actual database update."""
220
-
221
- updated_count = 0
222
- created_count = 0
223
- errors = []
224
-
225
- # Create lookup for fresh data
226
- fresh_data_map = {curr.code: curr for curr in fresh_currencies}
227
-
228
- try:
229
- with transaction.atomic():
230
- self.stdout.write("šŸ’¾ Updating database...")
231
-
232
- # Process currencies
233
- for i, fresh_currency in enumerate(fresh_currencies):
234
- try:
235
- currency, created = Currency.objects.update_or_create(
236
- code=fresh_currency.code,
237
- defaults={
238
- 'name': fresh_currency.name,
239
- 'symbol': fresh_currency.symbol,
240
- 'currency_type': fresh_currency.currency_type,
241
- 'decimal_places': fresh_currency.decimal_places,
242
- 'usd_rate': fresh_currency.usd_rate,
243
- 'min_payment_amount': fresh_currency.min_payment_amount,
244
- 'is_active': fresh_currency.is_active,
245
- 'rate_updated_at': timezone.now()
246
- }
247
- )
248
-
249
- if created:
250
- created_count += 1
251
- if options['verbose']:
252
- self.stdout.write(f" āž• Created {fresh_currency.code}")
253
- else:
254
- updated_count += 1
255
- if options['verbose']:
256
- self.stdout.write(f" šŸ”„ Updated {fresh_currency.code}")
257
-
258
- # Progress indicator
259
- if (i + 1) % 50 == 0:
260
- self.stdout.write(f" Progress: {i + 1}/{len(fresh_currencies)} currencies processed")
261
-
262
- except Exception as e:
263
- error_msg = f"Failed to update {fresh_currency.code}: {str(e)}"
264
- errors.append(error_msg)
265
- logger.error(error_msg)
266
-
267
- # Continue with other currencies unless it's a critical error
268
- if len(errors) > 10: # Too many errors
269
- raise CommandError(f"Too many errors ({len(errors)}), aborting")
270
-
271
- # Summary
272
- total_processed = created_count + updated_count
273
- self.stdout.write(f"\nšŸ“Š Update Summary:")
274
- self.stdout.write(f" āœ… Successfully processed: {total_processed} currencies")
275
- self.stdout.write(f" āž• Created new: {created_count}")
276
- self.stdout.write(f" šŸ”„ Updated existing: {updated_count}")
277
-
278
- if errors:
279
- self.stdout.write(f" āš ļø Errors: {len(errors)}")
280
- for error in errors[:5]: # Show first 5 errors
281
- self.stdout.write(f" • {error}")
282
- if len(errors) > 5:
283
- self.stdout.write(f" ... and {len(errors) - 5} more errors")
284
-
285
- # Deactivate currencies not in fresh data (optional)
286
- fresh_codes = {curr.code for curr in fresh_currencies}
287
- stale_currencies = Currency.objects.filter(is_active=True).exclude(
288
- code__in=fresh_codes
289
- )
290
-
291
- if stale_currencies.exists():
292
- self.stdout.write(f" šŸ“‹ Found {stale_currencies.count()} currencies not in fresh data")
293
- # Optionally deactivate them
294
- # stale_currencies.update(is_active=False)
295
-
296
- self.stdout.write(
297
- self.style.SUCCESS('āœ… Currency database update completed successfully!')
298
- )
299
-
300
- except Exception as e:
301
- self.stdout.write(
302
- self.style.ERROR(f'āŒ Update failed and rolled back: {str(e)}')
303
- )
304
- raise
305
-
306
- def _show_statistics(self):
307
- """Show current currency statistics."""
308
- total = Currency.objects.count()
309
- fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
310
- crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
311
- active_count = Currency.objects.filter(is_active=True).count()
312
-
313
- # Recent updates
314
- recent_threshold = timezone.now() - timedelta(hours=24)
315
- recent_updates = Currency.objects.filter(rate_updated_at__gte=recent_threshold).count()
316
-
317
- self.stdout.write(f"\nšŸ“Š Current Database Statistics:")
318
- self.stdout.write(f" • Total currencies: {total}")
319
- self.stdout.write(f" • Fiat currencies: {fiat_count}")
320
- self.stdout.write(f" • Cryptocurrencies: {crypto_count}")
321
- self.stdout.write(f" • Active currencies: {active_count}")
322
- self.stdout.write(f" • Updated in last 24h: {recent_updates}")
323
-
324
- # Show some examples
325
- recent_currencies = Currency.objects.filter(
326
- rate_updated_at__gte=recent_threshold
327
- ).order_by('-rate_updated_at')[:5]
328
-
329
- if recent_currencies:
330
- self.stdout.write(f"\nšŸ•’ Recently updated currencies:")
331
- for currency in recent_currencies:
332
- age = timezone.now() - currency.rate_updated_at
333
- hours_ago = int(age.total_seconds() / 3600)
334
- self.stdout.write(
335
- f" • {currency.code}: ${currency.usd_rate:.6f} ({hours_ago}h ago)"
336
- )