django-cfg 1.2.31__py3-none-any.whl → 1.3.3__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/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -10
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +526 -222
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +465 -70
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/cleanup_expired_data.py +419 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +297 -225
- django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
- django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
- django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
- django_cfg/apps/payments/management/commands/test_providers.py +434 -0
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
- django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +13 -18
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +153 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +172 -148
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -285
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
- django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
- django_cfg/apps/payments/services/{cache/base.py → cache_service/interfaces.py} +3 -1
- django_cfg/apps/payments/services/cache_service/keys.py +49 -0
- django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
- django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +371 -465
- django_cfg/apps/payments/services/core/subscription_service.py +425 -481
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +234 -174
- django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
- django_cfg/apps/payments/services/providers/registry.py +367 -301
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +210 -129
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -103
- django_cfg/apps/payments/signals/subscription_signals.py +194 -142
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +45 -48
- django_cfg/apps/payments/urls_admin.py +33 -42
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +40 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -492
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +64 -16
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/utils/smart_defaults.py +227 -570
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/RECORD +162 -185
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -146
- django_cfg/apps/payments/managers/__init__.py +0 -23
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -306
- django_cfg/apps/payments/managers/payment_manager.py +0 -192
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -57
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -63
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/simple_cache.py +0 -135
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -461
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
- django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
- django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
- django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
- django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
- django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -635
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
- django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
- django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
- django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
- django_cfg/apps/payments/templates/payments/stats.html +0 -261
- django_cfg/apps/payments/templates/payments/test.html +0 -213
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/utils/__init__.py +0 -43
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -239
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -63
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -122
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -451
- django_cfg/apps/payments/views/templates/base.py +0 -212
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -158
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -244
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -66
- django_cfg/core/integration.py +0 -160
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,14 +1,7 @@
|
|
1
1
|
"""
|
2
|
-
Universal
|
2
|
+
Currency management command for Universal Payment System v2.0.
|
3
3
|
|
4
|
-
|
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
|
4
|
+
Integrates with django_currency module for automatic rate updates and population.
|
12
5
|
"""
|
13
6
|
|
14
7
|
from django.core.management.base import BaseCommand, CommandError
|
@@ -16,214 +9,373 @@ from django.db import transaction
|
|
16
9
|
from django.utils import timezone
|
17
10
|
from django.db.models import Q
|
18
11
|
from datetime import timedelta
|
19
|
-
from
|
12
|
+
from typing import List, Optional
|
20
13
|
import time
|
21
14
|
|
22
15
|
from django_cfg.modules.django_logger import get_logger
|
23
|
-
from django_cfg.modules.django_currency
|
24
|
-
|
25
|
-
|
16
|
+
from django_cfg.modules.django_currency import (
|
17
|
+
CurrencyConverter, convert_currency, get_exchange_rate,
|
18
|
+
CurrencyError, CurrencyNotFoundError
|
26
19
|
)
|
27
|
-
from django_cfg.apps.payments.models import Currency
|
20
|
+
from django_cfg.apps.payments.models import Currency, Network, ProviderCurrency
|
28
21
|
|
29
22
|
logger = get_logger("manage_currencies")
|
30
23
|
|
31
24
|
|
32
25
|
class Command(BaseCommand):
|
33
|
-
"""
|
26
|
+
"""
|
27
|
+
Universal currency management command using ready modules.
|
28
|
+
|
29
|
+
Features:
|
30
|
+
- Population of missing currencies
|
31
|
+
- USD rate updates using django_currency
|
32
|
+
- Provider currency synchronization
|
33
|
+
- Flexible filtering and options
|
34
|
+
"""
|
34
35
|
|
35
|
-
help = 'Manage currencies
|
36
|
+
help = 'Manage currencies and exchange rates for the payment system'
|
36
37
|
|
37
38
|
def add_arguments(self, parser):
|
38
|
-
"""Add command
|
39
|
+
"""Add command arguments."""
|
40
|
+
|
41
|
+
# Main operation modes
|
39
42
|
parser.add_argument(
|
40
43
|
'--populate',
|
41
44
|
action='store_true',
|
42
|
-
help='
|
45
|
+
help='Populate missing base currencies'
|
43
46
|
)
|
47
|
+
|
44
48
|
parser.add_argument(
|
45
49
|
'--rates-only',
|
46
50
|
action='store_true',
|
47
|
-
help='
|
51
|
+
help='Update USD exchange rates only (no population)'
|
48
52
|
)
|
53
|
+
|
49
54
|
parser.add_argument(
|
50
|
-
'--
|
51
|
-
|
52
|
-
|
53
|
-
help='Maximum number of cryptocurrencies to process (default: 200)'
|
55
|
+
'--sync-providers',
|
56
|
+
action='store_true',
|
57
|
+
help='Sync provider currencies after rate updates'
|
54
58
|
)
|
59
|
+
|
60
|
+
# Filtering options
|
55
61
|
parser.add_argument(
|
56
|
-
'--
|
57
|
-
type=
|
58
|
-
|
59
|
-
help='Maximum number of fiat currencies to process (default: 50)'
|
62
|
+
'--currency',
|
63
|
+
type=str,
|
64
|
+
help='Update specific currency code (e.g., BTC, ETH)'
|
60
65
|
)
|
66
|
+
|
67
|
+
parser.add_argument(
|
68
|
+
'--currency-type',
|
69
|
+
choices=['fiat', 'crypto'],
|
70
|
+
help='Filter by currency type'
|
71
|
+
)
|
72
|
+
|
73
|
+
parser.add_argument(
|
74
|
+
'--provider',
|
75
|
+
type=str,
|
76
|
+
help='Filter by provider name'
|
77
|
+
)
|
78
|
+
|
79
|
+
# Behavior options
|
61
80
|
parser.add_argument(
|
62
81
|
'--force',
|
63
82
|
action='store_true',
|
64
|
-
help='Force refresh
|
83
|
+
help='Force refresh rates even if recently updated'
|
65
84
|
)
|
85
|
+
|
66
86
|
parser.add_argument(
|
67
|
-
'--
|
68
|
-
|
69
|
-
help='
|
87
|
+
'--skip-existing',
|
88
|
+
action='store_true',
|
89
|
+
help='Skip currencies that already exist during population'
|
70
90
|
)
|
91
|
+
|
71
92
|
parser.add_argument(
|
72
93
|
'--dry-run',
|
73
94
|
action='store_true',
|
74
95
|
help='Show what would be done without making changes'
|
75
96
|
)
|
76
97
|
|
98
|
+
parser.add_argument(
|
99
|
+
'--limit',
|
100
|
+
type=int,
|
101
|
+
default=100,
|
102
|
+
help='Limit number of currencies to process (default: 100)'
|
103
|
+
)
|
104
|
+
|
77
105
|
def handle(self, *args, **options):
|
78
|
-
"""
|
106
|
+
"""Main command handler."""
|
107
|
+
|
79
108
|
start_time = time.time()
|
80
109
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
110
|
+
try:
|
111
|
+
self.stdout.write(
|
112
|
+
self.style.SUCCESS('🚀 Starting Universal Currency Management')
|
113
|
+
)
|
114
|
+
|
115
|
+
# Determine operation mode
|
116
|
+
if options['populate']:
|
117
|
+
result = self._populate_currencies(options)
|
118
|
+
elif options['rates_only']:
|
119
|
+
result = self._update_rates_only(options)
|
120
|
+
else:
|
121
|
+
# Default: populate + rates
|
122
|
+
self.stdout.write("📋 Running full currency management (populate + rates)")
|
123
|
+
populate_result = self._populate_currencies(options)
|
124
|
+
rates_result = self._update_rates_only(options)
|
125
|
+
result = populate_result + rates_result
|
126
|
+
|
127
|
+
# Optional provider sync
|
128
|
+
if options['sync_providers']:
|
129
|
+
self._sync_provider_currencies(options)
|
130
|
+
|
131
|
+
# Show summary
|
132
|
+
elapsed = time.time() - start_time
|
133
|
+
self.stdout.write(
|
134
|
+
self.style.SUCCESS(
|
135
|
+
f'✅ Currency management completed in {elapsed:.1f}s'
|
136
|
+
)
|
137
|
+
)
|
138
|
+
|
139
|
+
if not options['dry_run']:
|
140
|
+
self._show_final_stats()
|
141
|
+
|
142
|
+
except Exception as e:
|
143
|
+
self.stdout.write(
|
144
|
+
self.style.ERROR(f'❌ Currency management failed: {e}')
|
145
|
+
)
|
146
|
+
logger.error(f"Currency management command failed: {e}")
|
147
|
+
raise CommandError(f"Command failed: {e}")
|
148
|
+
|
149
|
+
def _populate_currencies(self, options) -> int:
|
150
|
+
"""Populate missing base currencies."""
|
151
|
+
|
152
|
+
self.stdout.write("📦 Populating base currencies...")
|
153
|
+
|
154
|
+
# Define standard currencies to populate
|
155
|
+
standard_currencies = [
|
156
|
+
# Major fiat currencies
|
157
|
+
('USD', 'US Dollar', Currency.CurrencyType.FIAT, '$', 2),
|
158
|
+
('EUR', 'Euro', Currency.CurrencyType.FIAT, '€', 2),
|
159
|
+
('GBP', 'British Pound', Currency.CurrencyType.FIAT, '£', 2),
|
160
|
+
('JPY', 'Japanese Yen', Currency.CurrencyType.FIAT, '¥', 0),
|
161
|
+
('CNY', 'Chinese Yuan', Currency.CurrencyType.FIAT, '¥', 2),
|
162
|
+
('RUB', 'Russian Ruble', Currency.CurrencyType.FIAT, '₽', 2),
|
163
|
+
|
164
|
+
# Major cryptocurrencies
|
165
|
+
('BTC', 'Bitcoin', Currency.CurrencyType.CRYPTO, '₿', 8),
|
166
|
+
('ETH', 'Ethereum', Currency.CurrencyType.CRYPTO, 'Ξ', 8),
|
167
|
+
('USDT', 'Tether USD', Currency.CurrencyType.CRYPTO, '₮', 6),
|
168
|
+
('USDC', 'USD Coin', Currency.CurrencyType.CRYPTO, '', 6),
|
169
|
+
('BNB', 'Binance Coin', Currency.CurrencyType.CRYPTO, '', 8),
|
170
|
+
('ADA', 'Cardano', Currency.CurrencyType.CRYPTO, '', 6),
|
171
|
+
('SOL', 'Solana', Currency.CurrencyType.CRYPTO, '', 8),
|
172
|
+
('DOT', 'Polkadot', Currency.CurrencyType.CRYPTO, '', 8),
|
173
|
+
('MATIC', 'Polygon', Currency.CurrencyType.CRYPTO, '', 8),
|
174
|
+
('LTC', 'Litecoin', Currency.CurrencyType.CRYPTO, 'Ł', 8),
|
175
|
+
('TRX', 'TRON', Currency.CurrencyType.CRYPTO, '', 6),
|
176
|
+
('XRP', 'Ripple', Currency.CurrencyType.CRYPTO, '', 6),
|
177
|
+
]
|
178
|
+
|
179
|
+
# Apply currency type filter
|
180
|
+
if options['currency_type']:
|
181
|
+
currency_type_filter = Currency.CurrencyType.FIAT if options['currency_type'] == 'fiat' else Currency.CurrencyType.CRYPTO
|
182
|
+
standard_currencies = [
|
183
|
+
c for c in standard_currencies
|
184
|
+
if c[2] == currency_type_filter
|
185
|
+
]
|
186
|
+
|
187
|
+
# Apply specific currency filter
|
188
|
+
if options['currency']:
|
189
|
+
currency_code = options['currency'].upper()
|
190
|
+
standard_currencies = [
|
191
|
+
c for c in standard_currencies
|
192
|
+
if c[0] == currency_code
|
193
|
+
]
|
194
|
+
|
195
|
+
if not standard_currencies:
|
196
|
+
raise CommandError(f"Currency '{currency_code}' not in standard list")
|
197
|
+
|
198
|
+
created_count = 0
|
199
|
+
skipped_count = 0
|
200
|
+
|
201
|
+
for code, name, currency_type, symbol, decimal_places in standard_currencies:
|
202
|
+
|
203
|
+
if options['dry_run']:
|
204
|
+
exists = Currency.objects.filter(code=code).exists()
|
205
|
+
if exists and options['skip_existing']:
|
206
|
+
self.stdout.write(f" [DRY RUN] Would skip existing {code}")
|
207
|
+
skipped_count += 1
|
208
|
+
else:
|
209
|
+
self.stdout.write(f" [DRY RUN] Would create/update {code}")
|
210
|
+
continue
|
211
|
+
|
212
|
+
try:
|
213
|
+
currency, created = Currency.objects.get_or_create(
|
214
|
+
code=code,
|
215
|
+
defaults={
|
216
|
+
'name': name,
|
217
|
+
'currency_type': currency_type,
|
218
|
+
'symbol': symbol,
|
219
|
+
'decimal_places': decimal_places,
|
220
|
+
'is_active': True
|
221
|
+
}
|
222
|
+
)
|
223
|
+
|
224
|
+
if created:
|
225
|
+
self.stdout.write(f" ✅ Created {code} - {name}")
|
226
|
+
created_count += 1
|
227
|
+
logger.info(f"Created currency: {code}")
|
228
|
+
elif not options['skip_existing']:
|
229
|
+
# Update existing currency if not skipping
|
230
|
+
currency.name = name
|
231
|
+
currency.symbol = symbol
|
232
|
+
currency.decimal_places = decimal_places
|
233
|
+
currency.save()
|
234
|
+
self.stdout.write(f" 🔄 Updated {code} - {name}")
|
235
|
+
else:
|
236
|
+
self.stdout.write(f" ⏭️ Skipped existing {code}")
|
237
|
+
skipped_count += 1
|
238
|
+
|
239
|
+
except Exception as e:
|
240
|
+
self.stdout.write(f" ❌ Failed to create {code}: {e}")
|
241
|
+
logger.error(f"Failed to create currency {code}: {e}")
|
242
|
+
|
243
|
+
self.stdout.write(f"📦 Population complete: {created_count} created, {skipped_count} skipped")
|
244
|
+
return created_count
|
245
|
+
|
246
|
+
def _update_rates_only(self, options) -> int:
|
247
|
+
"""Update USD exchange rates using django_currency module."""
|
248
|
+
|
104
249
|
self.stdout.write("💱 Updating USD exchange rates...")
|
105
250
|
|
251
|
+
# Build queryset based on options
|
252
|
+
queryset = Currency.objects.all()
|
253
|
+
|
106
254
|
if options['currency']:
|
107
|
-
|
108
|
-
if not
|
255
|
+
queryset = queryset.filter(code__iexact=options['currency'])
|
256
|
+
if not queryset.exists():
|
109
257
|
raise CommandError(f"Currency '{options['currency']}' not found")
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
)
|
258
|
+
|
259
|
+
if options['currency_type']:
|
260
|
+
currency_type = Currency.CurrencyType.FIAT if options['currency_type'] == 'fiat' else Currency.CurrencyType.CRYPTO
|
261
|
+
queryset = queryset.filter(currency_type=currency_type)
|
262
|
+
|
263
|
+
# Filter by staleness unless forced
|
264
|
+
if not options['force']:
|
265
|
+
stale_threshold = timezone.now() - timedelta(hours=12)
|
266
|
+
|
267
|
+
# Get currencies that need rate updates through ProviderCurrency
|
268
|
+
currencies_needing_update = Currency.objects.filter(
|
269
|
+
Q(providercurrency__usd_rate__isnull=True) |
|
270
|
+
Q(providercurrency__rate_updated_at__isnull=True) |
|
271
|
+
Q(providercurrency__rate_updated_at__lt=stale_threshold)
|
272
|
+
).distinct()
|
273
|
+
|
274
|
+
queryset = queryset.filter(id__in=currencies_needing_update)
|
275
|
+
|
276
|
+
# Apply limit
|
277
|
+
queryset = queryset[:options['limit']]
|
118
278
|
|
119
279
|
updated_count = 0
|
120
280
|
error_count = 0
|
121
281
|
|
122
|
-
self.stdout.write(f"📊 Processing {
|
282
|
+
self.stdout.write(f"📊 Processing {queryset.count()} currencies...")
|
123
283
|
|
124
|
-
for currency in
|
284
|
+
for currency in queryset:
|
285
|
+
|
125
286
|
if options['dry_run']:
|
126
287
|
self.stdout.write(f" [DRY RUN] Would update {currency.code}")
|
127
288
|
continue
|
128
|
-
|
289
|
+
|
129
290
|
try:
|
130
|
-
#
|
131
|
-
|
132
|
-
currency
|
133
|
-
|
134
|
-
|
291
|
+
# Use django_currency module to get rate
|
292
|
+
if currency.code == 'USD':
|
293
|
+
# USD is the base currency
|
294
|
+
usd_rate = 1.0
|
295
|
+
else:
|
296
|
+
# Get rate from django_currency
|
297
|
+
usd_rate = get_exchange_rate(currency.code, 'USD')
|
135
298
|
|
136
|
-
if
|
137
|
-
|
299
|
+
if usd_rate and usd_rate > 0:
|
300
|
+
# Update rate in ProviderCurrency (create if doesn't exist)
|
301
|
+
provider_currency, created = ProviderCurrency.objects.get_or_create(
|
302
|
+
currency=currency,
|
303
|
+
provider_name='system', # System-level rate
|
304
|
+
provider_currency_code=currency.code,
|
305
|
+
defaults={
|
306
|
+
'usd_rate': usd_rate,
|
307
|
+
'rate_updated_at': timezone.now(),
|
308
|
+
'is_enabled': True,
|
309
|
+
'is_stable': currency.currency_type == Currency.CurrencyType.FIAT
|
310
|
+
}
|
311
|
+
)
|
312
|
+
|
313
|
+
if not created:
|
314
|
+
provider_currency.usd_rate = usd_rate
|
315
|
+
provider_currency.rate_updated_at = timezone.now()
|
316
|
+
provider_currency.save(update_fields=['usd_rate', 'rate_updated_at'])
|
317
|
+
|
318
|
+
# Update currency's exchange rate source
|
319
|
+
currency.exchange_rate_source = 'django_currency'
|
320
|
+
currency.save(update_fields=['exchange_rate_source'])
|
321
|
+
|
322
|
+
self.stdout.write(f" ✅ {currency.code}: ${usd_rate:.8f}")
|
138
323
|
updated_count += 1
|
324
|
+
|
139
325
|
else:
|
140
326
|
self.stdout.write(f" ⚠️ {currency.code}: No rate available")
|
141
327
|
|
328
|
+
except (CurrencyError, CurrencyNotFoundError) as e:
|
329
|
+
self.stdout.write(f" ⚠️ {currency.code}: {str(e)}")
|
330
|
+
error_count += 1
|
142
331
|
except Exception as e:
|
143
332
|
self.stdout.write(f" ❌ {currency.code}: {str(e)}")
|
144
333
|
error_count += 1
|
334
|
+
logger.error(f"Failed to update rate for {currency.code}: {e}")
|
145
335
|
|
146
|
-
self.stdout.write(f"
|
336
|
+
self.stdout.write(f"💱 Rate update complete: {updated_count} updated, {error_count} errors")
|
147
337
|
return updated_count
|
338
|
+
|
339
|
+
def _sync_provider_currencies(self, options):
|
340
|
+
"""Sync provider currencies after rate updates."""
|
148
341
|
|
149
|
-
|
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)
|
342
|
+
self.stdout.write("🔄 Syncing provider currencies...")
|
177
343
|
|
178
344
|
try:
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
345
|
+
from django.core.management import call_command
|
346
|
+
|
347
|
+
if options['provider']:
|
348
|
+
call_command('manage_providers', '--provider', options['provider'])
|
349
|
+
else:
|
350
|
+
call_command('manage_providers', '--all')
|
210
351
|
|
211
|
-
|
212
|
-
logger.exception("Failed to populate currencies")
|
213
|
-
raise CommandError(f"Population failed: {e}")
|
352
|
+
self.stdout.write("🔄 Provider sync completed")
|
214
353
|
|
215
|
-
|
216
|
-
|
217
|
-
|
354
|
+
except Exception as e:
|
355
|
+
self.stdout.write(f"⚠️ Provider sync failed: {e}")
|
356
|
+
logger.warning(f"Provider sync failed: {e}")
|
357
|
+
|
358
|
+
def _show_final_stats(self):
|
359
|
+
"""Show final statistics."""
|
218
360
|
|
219
|
-
|
220
|
-
|
361
|
+
try:
|
362
|
+
total_currencies = Currency.objects.count()
|
363
|
+
fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
|
364
|
+
crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
|
365
|
+
active_count = Currency.objects.filter(is_active=True).count()
|
221
366
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
367
|
+
# Count currencies with rates
|
368
|
+
currencies_with_rates = Currency.objects.filter(
|
369
|
+
providercurrency__usd_rate__isnull=False
|
370
|
+
).distinct().count()
|
371
|
+
|
372
|
+
rate_coverage = (currencies_with_rates / total_currencies * 100) if total_currencies > 0 else 0
|
373
|
+
|
374
|
+
self.stdout.write("\n📊 Final Statistics:")
|
375
|
+
self.stdout.write(f" Total currencies: {total_currencies}")
|
376
|
+
self.stdout.write(f" Fiat: {fiat_count}, Crypto: {crypto_count}")
|
377
|
+
self.stdout.write(f" Active: {active_count}")
|
378
|
+
self.stdout.write(f" With USD rates: {currencies_with_rates} ({rate_coverage:.1f}%)")
|
379
|
+
|
380
|
+
except Exception as e:
|
381
|
+
logger.warning(f"Failed to show final stats: {e}")
|