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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/payments/admin/__init__.py +3 -2
- django_cfg/apps/payments/admin/balance_admin.py +18 -18
- django_cfg/apps/payments/admin/currencies_admin.py +319 -131
- django_cfg/apps/payments/admin/payments_admin.py +15 -4
- django_cfg/apps/payments/config/module.py +2 -2
- django_cfg/apps/payments/config/utils.py +2 -2
- django_cfg/apps/payments/decorators.py +2 -2
- django_cfg/apps/payments/management/commands/README.md +95 -127
- django_cfg/apps/payments/management/commands/currency_stats.py +5 -24
- django_cfg/apps/payments/management/commands/manage_currencies.py +229 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +235 -0
- django_cfg/apps/payments/managers/__init__.py +3 -2
- django_cfg/apps/payments/managers/balance_manager.py +2 -2
- django_cfg/apps/payments/managers/currency_manager.py +272 -49
- django_cfg/apps/payments/managers/payment_manager.py +161 -13
- django_cfg/apps/payments/middleware/api_access.py +2 -2
- django_cfg/apps/payments/middleware/rate_limiting.py +8 -18
- django_cfg/apps/payments/middleware/usage_tracking.py +20 -17
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +241 -0
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +30 -0
- django_cfg/apps/payments/models/__init__.py +3 -2
- django_cfg/apps/payments/models/currencies.py +187 -71
- django_cfg/apps/payments/models/payments.py +3 -2
- django_cfg/apps/payments/serializers/__init__.py +3 -2
- django_cfg/apps/payments/serializers/currencies.py +20 -12
- django_cfg/apps/payments/services/cache/simple_cache.py +2 -2
- django_cfg/apps/payments/services/core/balance_service.py +2 -2
- django_cfg/apps/payments/services/core/fallback_service.py +2 -2
- django_cfg/apps/payments/services/core/payment_service.py +3 -6
- django_cfg/apps/payments/services/core/subscription_service.py +4 -7
- django_cfg/apps/payments/services/internal_types.py +171 -7
- django_cfg/apps/payments/services/monitoring/api_schemas.py +58 -204
- django_cfg/apps/payments/services/monitoring/provider_health.py +2 -2
- django_cfg/apps/payments/services/providers/base.py +144 -43
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptapi/config.py +8 -0
- django_cfg/apps/payments/services/providers/cryptapi/models.py +192 -0
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +439 -0
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/cryptomus/models.py +176 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +429 -0
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +564 -0
- django_cfg/apps/payments/services/providers/models/__init__.py +34 -0
- django_cfg/apps/payments/services/providers/models/currencies.py +190 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +196 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +380 -0
- django_cfg/apps/payments/services/providers/registry.py +294 -11
- django_cfg/apps/payments/services/providers/stripe/__init__.py +4 -0
- django_cfg/apps/payments/services/providers/stripe/models.py +184 -0
- django_cfg/apps/payments/services/providers/stripe/provider.py +109 -0
- django_cfg/apps/payments/services/security/error_handler.py +6 -8
- django_cfg/apps/payments/services/security/payment_notifications.py +2 -2
- django_cfg/apps/payments/services/security/webhook_validator.py +3 -4
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/payment_signals.py +11 -5
- django_cfg/apps/payments/signals/subscription_signals.py +2 -2
- django_cfg/apps/payments/static/payments/css/payments.css +340 -0
- django_cfg/apps/payments/static/payments/js/notifications.js +202 -0
- django_cfg/apps/payments/static/payments/js/payment-utils.js +318 -0
- django_cfg/apps/payments/static/payments/js/theme.js +86 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +2 -2
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +50 -0
- django_cfg/apps/payments/templates/payments/base.html +182 -0
- django_cfg/apps/payments/templates/payments/components/payment_card.html +201 -0
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +109 -0
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +43 -0
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +40 -0
- django_cfg/apps/payments/templates/payments/components/status_badge.html +34 -0
- django_cfg/apps/payments/templates/payments/components/status_overview.html +148 -0
- django_cfg/apps/payments/templates/payments/dashboard.html +258 -0
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +35 -0
- django_cfg/apps/payments/templates/payments/payment_create.html +579 -0
- django_cfg/apps/payments/templates/payments/payment_detail.html +373 -0
- django_cfg/apps/payments/templates/payments/payment_list.html +354 -0
- django_cfg/apps/payments/templates/payments/stats.html +261 -0
- django_cfg/apps/payments/templates/payments/test.html +213 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -0
- django_cfg/apps/payments/templatetags/payments_tags.py +315 -0
- django_cfg/apps/payments/urls.py +3 -1
- django_cfg/apps/payments/urls_admin.py +58 -0
- django_cfg/apps/payments/utils/__init__.py +1 -3
- django_cfg/apps/payments/utils/billing_utils.py +2 -2
- django_cfg/apps/payments/utils/config_utils.py +2 -8
- django_cfg/apps/payments/utils/validation_utils.py +2 -2
- django_cfg/apps/payments/views/__init__.py +3 -2
- django_cfg/apps/payments/views/currency_views.py +31 -20
- django_cfg/apps/payments/views/payment_views.py +2 -2
- django_cfg/apps/payments/views/templates/__init__.py +25 -0
- django_cfg/apps/payments/views/templates/ajax.py +451 -0
- django_cfg/apps/payments/views/templates/base.py +212 -0
- django_cfg/apps/payments/views/templates/dashboard.py +60 -0
- django_cfg/apps/payments/views/templates/payment_detail.py +102 -0
- django_cfg/apps/payments/views/templates/payment_management.py +158 -0
- django_cfg/apps/payments/views/templates/qr_code.py +174 -0
- django_cfg/apps/payments/views/templates/stats.py +244 -0
- django_cfg/apps/payments/views/templates/utils.py +181 -0
- django_cfg/apps/payments/views/webhook_views.py +2 -2
- django_cfg/apps/payments/viewsets.py +3 -2
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +6 -3
- django_cfg/core/config.py +35 -0
- django_cfg/models/payments.py +2 -8
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_unfold/dashboard.py +7 -2
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/.gitignore +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- {django_cfg-1.2.27.dist-info ā django_cfg-1.2.31.dist-info}/METADATA +13 -18
- {django_cfg-1.2.27.dist-info ā django_cfg-1.2.31.dist-info}/RECORD +130 -83
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -310
- django_cfg/apps/payments/services/providers/nowpayments.py +0 -293
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- {django_cfg-1.2.27.dist-info ā django_cfg-1.2.31.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.27.dist-info ā django_cfg-1.2.31.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.27.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,
|
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
|
-
'
|
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
|
15
|
+
from django_cfg.modules.django_logger import get_logger
|
16
16
|
|
17
|
-
logger =
|
17
|
+
logger = get_logger("balance_manager")
|
18
18
|
|
19
19
|
|
20
20
|
class UserBalanceManager(models.Manager):
|