django-cfg 1.2.29__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 (126) 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/tasks/webhook_processing.py +2 -2
  60. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
  61. django_cfg/apps/payments/templates/payments/base.html +4 -4
  62. django_cfg/apps/payments/templates/payments/components/payment_card.html +6 -6
  63. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +4 -4
  64. django_cfg/apps/payments/templates/payments/components/progress_bar.html +14 -7
  65. django_cfg/apps/payments/templates/payments/components/provider_stats.html +2 -2
  66. django_cfg/apps/payments/templates/payments/components/status_badge.html +8 -1
  67. django_cfg/apps/payments/templates/payments/components/status_overview.html +34 -30
  68. django_cfg/apps/payments/templates/payments/dashboard.html +202 -290
  69. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
  70. django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
  71. django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
  72. django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
  73. django_cfg/apps/payments/templates/payments/stats.html +261 -0
  74. django_cfg/apps/payments/templates/payments/test.html +213 -0
  75. django_cfg/apps/payments/urls.py +3 -1
  76. django_cfg/apps/payments/{urls_templates.py → urls_admin.py} +6 -0
  77. django_cfg/apps/payments/utils/__init__.py +1 -3
  78. django_cfg/apps/payments/utils/billing_utils.py +2 -2
  79. django_cfg/apps/payments/utils/config_utils.py +2 -8
  80. django_cfg/apps/payments/utils/validation_utils.py +2 -2
  81. django_cfg/apps/payments/views/__init__.py +3 -2
  82. django_cfg/apps/payments/views/currency_views.py +31 -20
  83. django_cfg/apps/payments/views/payment_views.py +2 -2
  84. django_cfg/apps/payments/views/templates/ajax.py +141 -2
  85. django_cfg/apps/payments/views/templates/base.py +21 -13
  86. django_cfg/apps/payments/views/templates/payment_detail.py +1 -1
  87. django_cfg/apps/payments/views/templates/payment_management.py +34 -40
  88. django_cfg/apps/payments/views/templates/stats.py +8 -4
  89. django_cfg/apps/payments/views/webhook_views.py +2 -2
  90. django_cfg/apps/payments/viewsets.py +3 -2
  91. django_cfg/apps/tasks/urls.py +0 -2
  92. django_cfg/apps/tasks/urls_admin.py +14 -0
  93. django_cfg/apps/urls.py +4 -4
  94. django_cfg/core/config.py +35 -0
  95. django_cfg/models/payments.py +2 -8
  96. django_cfg/modules/django_currency/__init__.py +16 -11
  97. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  98. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  99. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  100. django_cfg/modules/django_currency/core/__init__.py +1 -7
  101. django_cfg/modules/django_currency/core/converter.py +18 -23
  102. django_cfg/modules/django_currency/core/models.py +122 -11
  103. django_cfg/modules/django_currency/database/__init__.py +4 -4
  104. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  105. django_cfg/modules/django_unfold/dashboard.py +7 -2
  106. django_cfg/template_archive/django_sample.zip +0 -0
  107. django_cfg/templates/admin/components/action_grid.html +9 -9
  108. django_cfg/templates/admin/components/metric_card.html +5 -5
  109. django_cfg/templates/admin/components/status_badge.html +2 -2
  110. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  111. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  112. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  113. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  114. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/METADATA +2 -4
  115. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/RECORD +118 -96
  116. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  117. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  118. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  119. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  120. django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
  121. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  122. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  123. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  124. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/WHEEL +0 -0
  125. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
  126. {django_cfg-1.2.29.dist-info → django_cfg-1.2.31.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,229 @@
1
+ """
2
+ Universal currency management command.
3
+
4
+ Combines populate_currencies, update_currencies, and update_currency_rates into one.
5
+
6
+ Usage:
7
+ python manage.py manage_currencies # Update existing currencies and rates
8
+ python manage.py manage_currencies --populate # Initial population + rates
9
+ python manage.py manage_currencies --rates-only # Only update USD rates
10
+ python manage.py manage_currencies --max-crypto 50 # Limit crypto currencies
11
+ python manage.py manage_currencies --force # Force refresh all data
12
+ """
13
+
14
+ from django.core.management.base import BaseCommand, CommandError
15
+ from django.db import transaction
16
+ from django.utils import timezone
17
+ from django.db.models import Q
18
+ from datetime import timedelta
19
+ from decimal import Decimal
20
+ import time
21
+
22
+ from django_cfg.modules.django_logger import get_logger
23
+ from django_cfg.modules.django_currency.database.database_loader import (
24
+ create_database_loader,
25
+ DatabaseLoaderConfig
26
+ )
27
+ from django_cfg.apps.payments.models import Currency
28
+
29
+ logger = get_logger("manage_currencies")
30
+
31
+
32
+ class Command(BaseCommand):
33
+ """Universal currency management command."""
34
+
35
+ help = 'Manage currencies: populate, update, and refresh USD rates'
36
+
37
+ def add_arguments(self, parser):
38
+ """Add command line arguments."""
39
+ parser.add_argument(
40
+ '--populate',
41
+ action='store_true',
42
+ help='Initial population mode (for empty database)'
43
+ )
44
+ parser.add_argument(
45
+ '--rates-only',
46
+ action='store_true',
47
+ help='Only update USD exchange rates'
48
+ )
49
+ parser.add_argument(
50
+ '--max-crypto',
51
+ type=int,
52
+ default=200,
53
+ help='Maximum number of cryptocurrencies to process (default: 200)'
54
+ )
55
+ parser.add_argument(
56
+ '--max-fiat',
57
+ type=int,
58
+ default=50,
59
+ help='Maximum number of fiat currencies to process (default: 50)'
60
+ )
61
+ parser.add_argument(
62
+ '--force',
63
+ action='store_true',
64
+ help='Force refresh all data even if fresh'
65
+ )
66
+ parser.add_argument(
67
+ '--currency',
68
+ type=str,
69
+ help='Update specific currency by code (e.g., BTC, ETH)'
70
+ )
71
+ parser.add_argument(
72
+ '--dry-run',
73
+ action='store_true',
74
+ help='Show what would be done without making changes'
75
+ )
76
+
77
+ def handle(self, *args, **options):
78
+ """Execute the command."""
79
+ start_time = time.time()
80
+
81
+ self.stdout.write('=' * 60)
82
+ self.stdout.write(self.style.SUCCESS('šŸš€ Currency Management Tool'))
83
+ self.stdout.write('=' * 60)
84
+
85
+ # Determine mode
86
+ if options['rates_only']:
87
+ result = self._update_rates_only(options)
88
+ elif options['populate']:
89
+ result = self._populate_and_update(options)
90
+ else:
91
+ result = self._update_existing(options)
92
+
93
+ # Show summary
94
+ elapsed = time.time() - start_time
95
+ self.stdout.write('=' * 60)
96
+ self.stdout.write(f"ā±ļø Completed in {elapsed:.2f} seconds")
97
+ self.stdout.write('=' * 60)
98
+
99
+ # Commands should not return values to stdout
100
+ pass
101
+
102
+ def _update_rates_only(self, options):
103
+ """Update only USD exchange rates."""
104
+ self.stdout.write("šŸ’± Updating USD exchange rates...")
105
+
106
+ if options['currency']:
107
+ currencies = Currency.objects.filter(code__iexact=options['currency'])
108
+ if not currencies.exists():
109
+ raise CommandError(f"Currency '{options['currency']}' not found")
110
+ else:
111
+ # Update all currencies, prioritizing those without rates or stale rates
112
+ stale_threshold = timezone.now() - timedelta(days=1)
113
+ currencies = Currency.objects.filter(
114
+ Q(usd_rate__isnull=True) |
115
+ Q(rate_updated_at__isnull=True) |
116
+ Q(rate_updated_at__lt=stale_threshold)
117
+ )
118
+
119
+ updated_count = 0
120
+ error_count = 0
121
+
122
+ self.stdout.write(f"šŸ“Š Processing {currencies.count()} currencies...")
123
+
124
+ for currency in currencies:
125
+ if options['dry_run']:
126
+ self.stdout.write(f" [DRY RUN] Would update {currency.code}")
127
+ continue
128
+
129
+ try:
130
+ # Force refresh if requested
131
+ rate = Currency.objects.get_usd_rate(
132
+ currency.code,
133
+ force_refresh=options['force']
134
+ )
135
+
136
+ if rate > 0:
137
+ self.stdout.write(f" āœ… {currency.code}: ${rate:.8f}")
138
+ updated_count += 1
139
+ else:
140
+ self.stdout.write(f" āš ļø {currency.code}: No rate available")
141
+
142
+ except Exception as e:
143
+ self.stdout.write(f" āŒ {currency.code}: {str(e)}")
144
+ error_count += 1
145
+
146
+ self.stdout.write(f"šŸ“ˆ Updated: {updated_count}, Errors: {error_count}")
147
+ return updated_count
148
+
149
+ def _populate_and_update(self, options):
150
+ """Initial population of currencies."""
151
+ self.stdout.write("šŸ”§ Populating currencies from external APIs...")
152
+
153
+ # Check if database is empty
154
+ existing_count = Currency.objects.count()
155
+ if existing_count > 0 and not options['force']:
156
+ self.stdout.write(
157
+ self.style.WARNING(
158
+ f"āš ļø Database already contains {existing_count} currencies. "
159
+ "Use --force to repopulate."
160
+ )
161
+ )
162
+ return 0
163
+
164
+ if options['dry_run']:
165
+ self.stdout.write("[DRY RUN] Would populate currencies...")
166
+ return 0
167
+
168
+ # Create database loader
169
+ config = DatabaseLoaderConfig(
170
+ max_crypto_currencies=options['max_crypto'],
171
+ max_fiat_currencies=options['max_fiat'],
172
+ yahoo_delay=1.0,
173
+ coinpaprika_delay=0.5
174
+ )
175
+
176
+ loader = create_database_loader(config)
177
+
178
+ try:
179
+ with transaction.atomic():
180
+ # Load currency data
181
+ currency_data = loader.build_currency_database_data()
182
+
183
+ created_count = 0
184
+ updated_count = 0
185
+
186
+ for currency_info in currency_data:
187
+ currency, created = Currency.objects.get_or_create_normalized(
188
+ code=currency_info.code,
189
+ defaults={
190
+ 'name': currency_info.name,
191
+ 'currency_type': currency_info.currency_type,
192
+ 'usd_rate': currency_info.rate,
193
+ 'rate_updated_at': timezone.now()
194
+ }
195
+ )
196
+
197
+ if created:
198
+ created_count += 1
199
+ self.stdout.write(f" āž• Created: {currency.code} - {currency.name}")
200
+ else:
201
+ # Update rate
202
+ currency.usd_rate = currency_info.rate
203
+ currency.rate_updated_at = timezone.now()
204
+ currency.save()
205
+ updated_count += 1
206
+ self.stdout.write(f" šŸ”„ Updated: {currency.code} - ${currency.usd_rate:.8f}")
207
+
208
+ self.stdout.write(f"šŸ“Š Created: {created_count}, Updated: {updated_count}")
209
+ return created_count + updated_count
210
+
211
+ except Exception as e:
212
+ logger.exception("Failed to populate currencies")
213
+ raise CommandError(f"Population failed: {e}")
214
+
215
+ def _update_existing(self, options):
216
+ """Update existing currencies and rates."""
217
+ self.stdout.write("šŸ”„ Updating existing currencies...")
218
+
219
+ if options['currency']:
220
+ return self._update_rates_only(options)
221
+
222
+ # First update currency metadata if needed
223
+ self.stdout.write("1ļøāƒ£ Checking currency metadata...")
224
+
225
+ # Then update rates
226
+ self.stdout.write("2ļøāƒ£ Updating USD exchange rates...")
227
+ rate_updates = self._update_rates_only(options)
228
+
229
+ return rate_updates
@@ -0,0 +1,235 @@
1
+ """
2
+ Universal payment provider management command.
3
+
4
+ Combines sync_providers functionality with additional features.
5
+
6
+ Usage:
7
+ python manage.py manage_providers # Sync all active providers
8
+ python manage.py manage_providers --provider nowpayments # Sync specific provider
9
+ python manage.py manage_providers --with-rates # Sync providers + update USD rates
10
+ python manage.py manage_providers --stats # Show provider statistics
11
+ """
12
+
13
+ from django.core.management.base import BaseCommand, CommandError
14
+ from django.db import transaction
15
+ from django.utils import timezone
16
+ from typing import List, Optional
17
+ import time
18
+
19
+ from django_cfg.modules.django_logger import get_logger
20
+ from django_cfg.apps.payments.services.providers.registry import get_payment_provider, get_available_providers
21
+ from django_cfg.apps.payments.models import Currency, ProviderCurrency
22
+
23
+ logger = get_logger("manage_providers")
24
+
25
+
26
+ class Command(BaseCommand):
27
+ """Universal payment provider management command."""
28
+
29
+ help = 'Manage payment providers: sync currencies, networks, and rates'
30
+
31
+ def add_arguments(self, parser):
32
+ """Add command line arguments."""
33
+ parser.add_argument(
34
+ '--provider',
35
+ type=str,
36
+ help='Specific provider(s) to sync (comma-separated). E.g: nowpayments,cryptomus'
37
+ )
38
+ parser.add_argument(
39
+ '--all',
40
+ action='store_true',
41
+ help='Sync all available providers'
42
+ )
43
+ parser.add_argument(
44
+ '--with-rates',
45
+ action='store_true',
46
+ help='Also update USD exchange rates after sync'
47
+ )
48
+ parser.add_argument(
49
+ '--stats',
50
+ action='store_true',
51
+ help='Show provider statistics'
52
+ )
53
+ parser.add_argument(
54
+ '--dry-run',
55
+ action='store_true',
56
+ help='Show what would be synced without making changes'
57
+ )
58
+ parser.add_argument(
59
+ '--verbose',
60
+ action='store_true',
61
+ help='Show detailed progress information'
62
+ )
63
+
64
+ def handle(self, *args, **options):
65
+ """Execute the command."""
66
+ start_time = time.time()
67
+
68
+ self.stdout.write('=' * 60)
69
+ self.stdout.write(self.style.SUCCESS('šŸš€ Provider Management Tool'))
70
+ self.stdout.write('=' * 60)
71
+
72
+ if options['stats']:
73
+ return self._show_stats()
74
+
75
+ # Determine which providers to sync
76
+ if options['provider']:
77
+ provider_names = [p.strip() for p in options['provider'].split(',')]
78
+ elif options['all']:
79
+ provider_names = get_available_providers()
80
+ else:
81
+ # Default: sync active providers only
82
+ provider_names = get_available_providers()
83
+
84
+ # Sync providers
85
+ total_synced = 0
86
+ for provider_name in provider_names:
87
+ synced = self._sync_provider(provider_name, options)
88
+ total_synced += synced
89
+
90
+ # Update rates if requested
91
+ if options['with_rates'] and not options['dry_run']:
92
+ self.stdout.write("\nšŸ’± Updating USD exchange rates...")
93
+ self._update_rates()
94
+
95
+ # Show summary
96
+ elapsed = time.time() - start_time
97
+ self.stdout.write('=' * 60)
98
+ self.stdout.write(f"šŸ“Š Total items synced: {total_synced}")
99
+ self.stdout.write(f"ā±ļø Completed in {elapsed:.2f} seconds")
100
+ self.stdout.write('=' * 60)
101
+
102
+ # Commands should not return values to stdout
103
+ pass
104
+
105
+ def _sync_provider(self, provider_name: str, options: dict) -> int:
106
+ """Sync a specific provider."""
107
+ self.stdout.write(f"\nšŸ”„ Syncing {provider_name}...")
108
+
109
+ try:
110
+ provider = get_payment_provider(provider_name)
111
+
112
+ if options['verbose']:
113
+ config = provider.config
114
+ self.stdout.write(f" šŸ“” Provider: {provider.__class__.__name__}")
115
+ self.stdout.write(f" šŸ”§ Config: enabled={config.enabled} timeout_seconds={config.timeout_seconds} sandbox={getattr(config, 'sandbox', 'N/A')}")
116
+
117
+ if options['dry_run']:
118
+ # Dry run: just get parsed currencies to show what would be synced
119
+ try:
120
+ parsed_response = provider.get_parsed_currencies()
121
+ currency_count = len(parsed_response.currencies)
122
+
123
+ # Calculate unique networks
124
+ networks = set()
125
+ for currency in parsed_response.currencies:
126
+ if currency.network_code:
127
+ networks.add(currency.network_code)
128
+ network_count = len(networks)
129
+
130
+ self.stdout.write(f" šŸ’° Would sync {currency_count} currencies")
131
+ self.stdout.write(f" 🌐 Would sync {network_count} networks")
132
+
133
+ return currency_count + network_count
134
+
135
+ except Exception as e:
136
+ self.stdout.write(f" āŒ Failed to fetch currencies: {e}")
137
+ return 0
138
+
139
+ else:
140
+ # Live sync
141
+ with transaction.atomic():
142
+ sync_result = provider.sync_currencies_to_db()
143
+
144
+ if options['verbose']:
145
+ self.stdout.write(f" āœ… Synced {sync_result.total_items_processed} items")
146
+ if sync_result.errors:
147
+ self.stdout.write(f" āš ļø Errors: {len(sync_result.errors)}")
148
+ for error in sync_result.errors[:3]: # Show first 3 errors
149
+ self.stdout.write(f" • {error}")
150
+
151
+ self.stdout.write(
152
+ self.style.SUCCESS(f"āœ… {provider_name}: {sync_result.total_items_processed} items synced")
153
+ )
154
+
155
+ return sync_result.total_items_processed
156
+
157
+ except Exception as e:
158
+ logger.exception(f"Error syncing provider {provider_name}")
159
+ self.stdout.write(
160
+ self.style.ERROR(f"āŒ Failed to sync {provider_name}: {e}")
161
+ )
162
+ return 0
163
+
164
+ def _update_rates(self):
165
+ """Update USD rates for currencies."""
166
+ try:
167
+ # Get currencies that need rate updates
168
+ from datetime import timedelta
169
+ from django.db.models import Q
170
+
171
+ stale_threshold = timezone.now() - timedelta(hours=12)
172
+ currencies_to_update = Currency.objects.filter(
173
+ Q(usd_rate__isnull=True) |
174
+ Q(rate_updated_at__isnull=True) |
175
+ Q(rate_updated_at__lt=stale_threshold)
176
+ )[:50] # Limit to avoid long execution
177
+
178
+ updated_count = 0
179
+ for currency in currencies_to_update:
180
+ try:
181
+ rate = Currency.objects.get_usd_rate(currency.code, force_refresh=True)
182
+ if rate > 0:
183
+ updated_count += 1
184
+ self.stdout.write(f" āœ… {currency.code}: ${rate:.8f}")
185
+ except Exception as e:
186
+ self.stdout.write(f" āš ļø {currency.code}: {str(e)}")
187
+
188
+ self.stdout.write(f"šŸ’± Updated {updated_count} exchange rates")
189
+
190
+ except Exception as e:
191
+ self.stdout.write(f"āš ļø Rate update failed: {e}")
192
+
193
+ def _show_stats(self):
194
+ """Show provider statistics."""
195
+ self.stdout.write("šŸ“Š Provider Statistics")
196
+ self.stdout.write("-" * 40)
197
+
198
+ # Available providers
199
+ available_providers = get_available_providers()
200
+ self.stdout.write(f"šŸ¢ Available providers: {len(available_providers)}")
201
+ for provider_name in available_providers:
202
+ try:
203
+ provider = get_payment_provider(provider_name)
204
+ enabled = provider.config.enabled
205
+ status = "āœ… Enabled" if enabled else "āŒ Disabled"
206
+ self.stdout.write(f" • {provider_name}: {status}")
207
+ except Exception as e:
208
+ self.stdout.write(f" • {provider_name}: āŒ Error ({e})")
209
+
210
+ self.stdout.write()
211
+
212
+ # Database statistics
213
+ total_currencies = Currency.objects.count()
214
+ total_provider_currencies = ProviderCurrency.objects.count()
215
+
216
+ self.stdout.write(f"šŸ’° Total currencies: {total_currencies}")
217
+ self.stdout.write(f"šŸ”— Total provider currencies: {total_provider_currencies}")
218
+
219
+ # Provider breakdown
220
+ from django.db.models import Count
221
+ provider_stats = ProviderCurrency.objects.values('provider_name').annotate(
222
+ count=Count('id')
223
+ ).order_by('-count')
224
+
225
+ self.stdout.write("\nšŸ“Š Currencies by provider:")
226
+ for stat in provider_stats:
227
+ self.stdout.write(f" • {stat['provider_name']}: {stat['count']} currencies")
228
+
229
+ # Rate statistics
230
+ currencies_with_rates = Currency.objects.exclude(usd_rate__isnull=True).exclude(usd_rate=0)
231
+ rate_coverage = (currencies_with_rates.count() / total_currencies * 100) if total_currencies > 0 else 0
232
+
233
+ self.stdout.write(f"\nšŸ’µ USD rate coverage: {rate_coverage:.1f}% ({currencies_with_rates.count()}/{total_currencies})")
234
+
235
+ # Stats command should not return values
@@ -7,7 +7,7 @@ from .balance_manager import UserBalanceManager
7
7
  from .subscription_manager import SubscriptionManager, EndpointGroupManager
8
8
  from .tariff_manager import TariffManager, TariffEndpointGroupManager
9
9
  from .api_key_manager import APIKeyManager
10
- from .currency_manager import CurrencyManager, CurrencyNetworkManager
10
+ from .currency_manager import CurrencyManager, NetworkManager, ProviderCurrencyManager
11
11
 
12
12
  __all__ = [
13
13
  'UniversalPaymentManager',
@@ -18,5 +18,6 @@ __all__ = [
18
18
  'TariffEndpointGroupManager',
19
19
  'APIKeyManager',
20
20
  'CurrencyManager',
21
- 'CurrencyNetworkManager',
21
+ 'NetworkManager',
22
+ 'ProviderCurrencyManager',
22
23
  ]
@@ -12,9 +12,9 @@ from django.db import models, transaction
12
12
  from django.utils import timezone
13
13
  from decimal import Decimal
14
14
  from typing import Optional, Dict, Any
15
- import logging
15
+ from django_cfg.modules.django_logger import get_logger
16
16
 
17
- logger = logging.getLogger(__name__)
17
+ logger = get_logger("balance_manager")
18
18
 
19
19
 
20
20
  class UserBalanceManager(models.Manager):