django-cfg 1.2.31__py3-none-any.whl → 1.3.1__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 (256) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/health/views.py +4 -2
  3. django_cfg/apps/knowbase/config/settings.py +16 -15
  4. django_cfg/apps/payments/README.md +326 -0
  5. django_cfg/apps/payments/admin/__init__.py +20 -10
  6. django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
  7. django_cfg/apps/payments/admin/balance_admin.py +592 -297
  8. django_cfg/apps/payments/admin/currencies_admin.py +526 -222
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +465 -70
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
  39. django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
  43. django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +13 -18
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +172 -148
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -285
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +346 -467
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -481
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +234 -174
  74. django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
  75. django_cfg/apps/payments/services/providers/registry.py +367 -301
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +210 -129
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +128 -103
  85. django_cfg/apps/payments/signals/subscription_signals.py +194 -142
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +45 -48
  93. django_cfg/apps/payments/urls_admin.py +33 -42
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/config.py +1 -1
  110. django_cfg/core/config.py +40 -4
  111. django_cfg/core/generation.py +25 -4
  112. django_cfg/core/integration/README.md +363 -0
  113. django_cfg/core/integration/__init__.py +47 -0
  114. django_cfg/core/integration/commands_collector.py +239 -0
  115. django_cfg/core/integration/display/__init__.py +15 -0
  116. django_cfg/core/integration/display/base.py +157 -0
  117. django_cfg/core/integration/display/ngrok.py +164 -0
  118. django_cfg/core/integration/display/startup.py +815 -0
  119. django_cfg/core/integration/url_integration.py +123 -0
  120. django_cfg/core/integration/version_checker.py +160 -0
  121. django_cfg/management/commands/auto_generate.py +4 -0
  122. django_cfg/management/commands/check_settings.py +6 -0
  123. django_cfg/management/commands/clear_constance.py +5 -2
  124. django_cfg/management/commands/create_token.py +6 -0
  125. django_cfg/management/commands/list_urls.py +6 -0
  126. django_cfg/management/commands/migrate_all.py +6 -0
  127. django_cfg/management/commands/migrator.py +3 -0
  128. django_cfg/management/commands/rundramatiq.py +6 -0
  129. django_cfg/management/commands/runserver_ngrok.py +51 -29
  130. django_cfg/management/commands/script.py +6 -0
  131. django_cfg/management/commands/show_config.py +12 -2
  132. django_cfg/management/commands/show_urls.py +4 -0
  133. django_cfg/management/commands/superuser.py +6 -0
  134. django_cfg/management/commands/task_clear.py +4 -1
  135. django_cfg/management/commands/task_status.py +3 -1
  136. django_cfg/management/commands/test_email.py +3 -0
  137. django_cfg/management/commands/test_telegram.py +6 -0
  138. django_cfg/management/commands/test_twilio.py +6 -0
  139. django_cfg/management/commands/tree.py +6 -0
  140. django_cfg/management/commands/validate_config.py +155 -149
  141. django_cfg/models/constance.py +31 -11
  142. django_cfg/models/payments.py +175 -492
  143. django_cfg/modules/django_logger.py +160 -146
  144. django_cfg/modules/django_unfold/dashboard.py +64 -16
  145. django_cfg/registry/core.py +1 -0
  146. django_cfg/template_archive/django_sample.zip +0 -0
  147. django_cfg/utils/smart_defaults.py +222 -571
  148. django_cfg/utils/toolkit.py +51 -11
  149. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
  150. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
  151. django_cfg/apps/payments/__init__.py +0 -8
  152. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  153. django_cfg/apps/payments/config/module.py +0 -70
  154. django_cfg/apps/payments/config/providers.py +0 -105
  155. django_cfg/apps/payments/config/settings.py +0 -96
  156. django_cfg/apps/payments/config/utils.py +0 -52
  157. django_cfg/apps/payments/decorators.py +0 -291
  158. django_cfg/apps/payments/management/commands/README.md +0 -146
  159. django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
  160. django_cfg/apps/payments/managers/__init__.py +0 -23
  161. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  162. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  163. django_cfg/apps/payments/managers/currency_manager.py +0 -306
  164. django_cfg/apps/payments/managers/payment_manager.py +0 -192
  165. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  166. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  167. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
  168. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
  169. django_cfg/apps/payments/models/events.py +0 -73
  170. django_cfg/apps/payments/serializers/__init__.py +0 -57
  171. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  172. django_cfg/apps/payments/serializers/balance.py +0 -59
  173. django_cfg/apps/payments/serializers/currencies.py +0 -63
  174. django_cfg/apps/payments/serializers/payments.py +0 -62
  175. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  176. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  177. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  178. django_cfg/apps/payments/services/cache/base.py +0 -30
  179. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  180. django_cfg/apps/payments/services/internal_types.py +0 -461
  181. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  182. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  183. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
  184. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  185. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
  186. django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
  187. django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
  188. django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
  189. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
  190. django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
  191. django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
  192. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
  193. django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
  194. django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
  195. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
  196. django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
  197. django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
  198. django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
  199. django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
  200. django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
  201. django_cfg/apps/payments/services/security/__init__.py +0 -34
  202. django_cfg/apps/payments/services/security/error_handler.py +0 -635
  203. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  204. django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
  205. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  206. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  207. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  208. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  209. django_cfg/apps/payments/tasks/__init__.py +0 -12
  210. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  211. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
  212. django_cfg/apps/payments/templates/payments/base.html +0 -182
  213. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  214. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  215. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
  216. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  217. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
  218. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
  219. django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
  220. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
  221. django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
  222. django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
  223. django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
  224. django_cfg/apps/payments/templates/payments/stats.html +0 -261
  225. django_cfg/apps/payments/templates/payments/test.html +0 -213
  226. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  227. django_cfg/apps/payments/utils/__init__.py +0 -43
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -239
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -63
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -122
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -451
  241. django_cfg/apps/payments/views/templates/base.py +0 -212
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -158
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -244
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -66
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/template_archive/.gitignore +0 -1
  252. django_cfg/template_archive/__init__.py +0 -0
  253. django_cfg/urls.py +0 -33
  254. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  255. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  256. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,14 +1,7 @@
1
1
  """
2
- Universal currency management command.
2
+ Currency management command for Universal Payment System v2.0.
3
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
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 decimal import Decimal
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.database.database_loader import (
24
- create_database_loader,
25
- DatabaseLoaderConfig
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
- """Universal currency management command."""
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: populate, update, and refresh USD rates'
36
+ help = 'Manage currencies and exchange rates for the payment system'
36
37
 
37
38
  def add_arguments(self, parser):
38
- """Add command line arguments."""
39
+ """Add command arguments."""
40
+
41
+ # Main operation modes
39
42
  parser.add_argument(
40
43
  '--populate',
41
44
  action='store_true',
42
- help='Initial population mode (for empty database)'
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='Only update USD exchange rates'
51
+ help='Update USD exchange rates only (no population)'
48
52
  )
53
+
49
54
  parser.add_argument(
50
- '--max-crypto',
51
- type=int,
52
- default=200,
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
- '--max-fiat',
57
- type=int,
58
- default=50,
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 all data even if fresh'
83
+ help='Force refresh rates even if recently updated'
65
84
  )
85
+
66
86
  parser.add_argument(
67
- '--currency',
68
- type=str,
69
- help='Update specific currency by code (e.g., BTC, ETH)'
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
- """Execute the command."""
106
+ """Main command handler."""
107
+
79
108
  start_time = time.time()
80
109
 
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."""
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
- currencies = Currency.objects.filter(code__iexact=options['currency'])
108
- if not currencies.exists():
255
+ queryset = queryset.filter(code__iexact=options['currency'])
256
+ if not queryset.exists():
109
257
  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
- )
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 {currencies.count()} currencies...")
282
+ self.stdout.write(f"📊 Processing {queryset.count()} currencies...")
123
283
 
124
- for currency in currencies:
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
- # Force refresh if requested
131
- rate = Currency.objects.get_usd_rate(
132
- currency.code,
133
- force_refresh=options['force']
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 rate > 0:
137
- self.stdout.write(f" ✅ {currency.code}: ${rate:.8f}")
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"📈 Updated: {updated_count}, Errors: {error_count}")
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
- 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)
342
+ self.stdout.write("🔄 Syncing provider currencies...")
177
343
 
178
344
  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
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
- except Exception as e:
212
- logger.exception("Failed to populate currencies")
213
- raise CommandError(f"Population failed: {e}")
352
+ self.stdout.write("🔄 Provider sync completed")
214
353
 
215
- def _update_existing(self, options):
216
- """Update existing currencies and rates."""
217
- self.stdout.write("🔄 Updating existing currencies...")
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
- if options['currency']:
220
- return self._update_rates_only(options)
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
- # 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
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}")