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.
Files changed (264) 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/cleanup_expired_data.py +419 -0
  39. django_cfg/apps/payments/management/commands/currency_stats.py +297 -225
  40. django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
  41. django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
  42. django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
  43. django_cfg/apps/payments/management/commands/test_providers.py +434 -0
  44. django_cfg/apps/payments/middleware/__init__.py +3 -1
  45. django_cfg/apps/payments/middleware/api_access.py +329 -222
  46. django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
  47. django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
  48. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  49. django_cfg/apps/payments/models/__init__.py +13 -18
  50. django_cfg/apps/payments/models/api_keys.py +121 -43
  51. django_cfg/apps/payments/models/balance.py +153 -115
  52. django_cfg/apps/payments/models/base.py +68 -15
  53. django_cfg/apps/payments/models/currencies.py +172 -148
  54. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  55. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  56. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  57. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  58. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  59. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  60. django_cfg/apps/payments/models/payments.py +235 -285
  61. django_cfg/apps/payments/models/subscriptions.py +257 -177
  62. django_cfg/apps/payments/models/tariffs.py +147 -40
  63. django_cfg/apps/payments/services/__init__.py +209 -56
  64. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  65. django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
  66. django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
  67. django_cfg/apps/payments/services/{cache/base.py → cache_service/interfaces.py} +3 -1
  68. django_cfg/apps/payments/services/cache_service/keys.py +49 -0
  69. django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
  70. django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
  71. django_cfg/apps/payments/services/core/__init__.py +10 -6
  72. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  73. django_cfg/apps/payments/services/core/base.py +166 -0
  74. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  75. django_cfg/apps/payments/services/core/payment_service.py +371 -465
  76. django_cfg/apps/payments/services/core/subscription_service.py +425 -481
  77. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  78. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  79. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  80. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  81. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  82. django_cfg/apps/payments/services/providers/base.py +234 -174
  83. django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
  84. django_cfg/apps/payments/services/providers/registry.py +367 -301
  85. django_cfg/apps/payments/services/types/__init__.py +78 -0
  86. django_cfg/apps/payments/services/types/data.py +177 -0
  87. django_cfg/apps/payments/services/types/requests.py +150 -0
  88. django_cfg/apps/payments/services/types/responses.py +156 -0
  89. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  90. django_cfg/apps/payments/signals/__init__.py +33 -8
  91. django_cfg/apps/payments/signals/api_key_signals.py +210 -129
  92. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  93. django_cfg/apps/payments/signals/payment_signals.py +128 -103
  94. django_cfg/apps/payments/signals/subscription_signals.py +194 -142
  95. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  96. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  97. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  98. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  99. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  100. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  101. django_cfg/apps/payments/urls.py +45 -48
  102. django_cfg/apps/payments/urls_admin.py +33 -42
  103. django_cfg/apps/payments/views/api/__init__.py +101 -0
  104. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  105. django_cfg/apps/payments/views/api/balances.py +381 -0
  106. django_cfg/apps/payments/views/api/base.py +298 -0
  107. django_cfg/apps/payments/views/api/currencies.py +402 -0
  108. django_cfg/apps/payments/views/api/payments.py +415 -0
  109. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  110. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  111. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  112. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  113. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  114. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  115. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  116. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  117. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  118. django_cfg/config.py +1 -1
  119. django_cfg/core/config.py +40 -4
  120. django_cfg/core/generation.py +25 -4
  121. django_cfg/core/integration/README.md +363 -0
  122. django_cfg/core/integration/__init__.py +47 -0
  123. django_cfg/core/integration/commands_collector.py +239 -0
  124. django_cfg/core/integration/display/__init__.py +15 -0
  125. django_cfg/core/integration/display/base.py +157 -0
  126. django_cfg/core/integration/display/ngrok.py +164 -0
  127. django_cfg/core/integration/display/startup.py +815 -0
  128. django_cfg/core/integration/url_integration.py +123 -0
  129. django_cfg/core/integration/version_checker.py +160 -0
  130. django_cfg/management/commands/auto_generate.py +4 -0
  131. django_cfg/management/commands/check_settings.py +6 -0
  132. django_cfg/management/commands/clear_constance.py +5 -2
  133. django_cfg/management/commands/create_token.py +6 -0
  134. django_cfg/management/commands/list_urls.py +6 -0
  135. django_cfg/management/commands/migrate_all.py +6 -0
  136. django_cfg/management/commands/migrator.py +3 -0
  137. django_cfg/management/commands/rundramatiq.py +6 -0
  138. django_cfg/management/commands/runserver_ngrok.py +51 -29
  139. django_cfg/management/commands/script.py +6 -0
  140. django_cfg/management/commands/show_config.py +12 -2
  141. django_cfg/management/commands/show_urls.py +4 -0
  142. django_cfg/management/commands/superuser.py +6 -0
  143. django_cfg/management/commands/task_clear.py +4 -1
  144. django_cfg/management/commands/task_status.py +3 -1
  145. django_cfg/management/commands/test_email.py +3 -0
  146. django_cfg/management/commands/test_telegram.py +6 -0
  147. django_cfg/management/commands/test_twilio.py +6 -0
  148. django_cfg/management/commands/tree.py +6 -0
  149. django_cfg/management/commands/validate_config.py +155 -149
  150. django_cfg/models/constance.py +31 -11
  151. django_cfg/models/payments.py +175 -492
  152. django_cfg/modules/django_logger.py +160 -146
  153. django_cfg/modules/django_unfold/dashboard.py +64 -16
  154. django_cfg/registry/core.py +1 -0
  155. django_cfg/template_archive/django_sample.zip +0 -0
  156. django_cfg/utils/smart_defaults.py +227 -570
  157. django_cfg/utils/toolkit.py +51 -11
  158. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/METADATA +4 -1
  159. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/RECORD +162 -185
  160. django_cfg/apps/payments/__init__.py +0 -8
  161. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  162. django_cfg/apps/payments/config/module.py +0 -70
  163. django_cfg/apps/payments/config/providers.py +0 -105
  164. django_cfg/apps/payments/config/settings.py +0 -96
  165. django_cfg/apps/payments/config/utils.py +0 -52
  166. django_cfg/apps/payments/decorators.py +0 -291
  167. django_cfg/apps/payments/management/commands/README.md +0 -146
  168. django_cfg/apps/payments/managers/__init__.py +0 -23
  169. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  170. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  171. django_cfg/apps/payments/managers/currency_manager.py +0 -306
  172. django_cfg/apps/payments/managers/payment_manager.py +0 -192
  173. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  174. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  175. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
  176. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
  177. django_cfg/apps/payments/models/events.py +0 -73
  178. django_cfg/apps/payments/serializers/__init__.py +0 -57
  179. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  180. django_cfg/apps/payments/serializers/balance.py +0 -59
  181. django_cfg/apps/payments/serializers/currencies.py +0 -63
  182. django_cfg/apps/payments/serializers/payments.py +0 -62
  183. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  184. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  185. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  186. django_cfg/apps/payments/services/cache/simple_cache.py +0 -135
  187. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  188. django_cfg/apps/payments/services/internal_types.py +0 -461
  189. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  190. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  191. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
  192. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  193. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
  194. django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
  195. django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
  196. django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
  197. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
  198. django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
  199. django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
  200. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
  201. django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
  202. django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
  203. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
  204. django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
  205. django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
  206. django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
  207. django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
  208. django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
  209. django_cfg/apps/payments/services/security/__init__.py +0 -34
  210. django_cfg/apps/payments/services/security/error_handler.py +0 -635
  211. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  212. django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
  213. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  214. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  215. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  216. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  217. django_cfg/apps/payments/tasks/__init__.py +0 -12
  218. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  219. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
  220. django_cfg/apps/payments/templates/payments/base.html +0 -182
  221. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  222. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  223. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
  224. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  225. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
  226. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
  227. django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
  228. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
  229. django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
  230. django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
  231. django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
  232. django_cfg/apps/payments/templates/payments/stats.html +0 -261
  233. django_cfg/apps/payments/templates/payments/test.html +0 -213
  234. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  235. django_cfg/apps/payments/utils/__init__.py +0 -43
  236. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  237. django_cfg/apps/payments/utils/config_utils.py +0 -239
  238. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  239. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  240. django_cfg/apps/payments/views/__init__.py +0 -63
  241. django_cfg/apps/payments/views/api_key_views.py +0 -164
  242. django_cfg/apps/payments/views/balance_views.py +0 -75
  243. django_cfg/apps/payments/views/currency_views.py +0 -122
  244. django_cfg/apps/payments/views/payment_views.py +0 -149
  245. django_cfg/apps/payments/views/subscription_views.py +0 -135
  246. django_cfg/apps/payments/views/tariff_views.py +0 -131
  247. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  248. django_cfg/apps/payments/views/templates/ajax.py +0 -451
  249. django_cfg/apps/payments/views/templates/base.py +0 -212
  250. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  251. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  252. django_cfg/apps/payments/views/templates/payment_management.py +0 -158
  253. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  254. django_cfg/apps/payments/views/templates/stats.py +0 -244
  255. django_cfg/apps/payments/views/templates/utils.py +0 -181
  256. django_cfg/apps/payments/views/webhook_views.py +0 -266
  257. django_cfg/apps/payments/viewsets.py +0 -66
  258. django_cfg/core/integration.py +0 -160
  259. django_cfg/template_archive/.gitignore +0 -1
  260. django_cfg/template_archive/__init__.py +0 -0
  261. django_cfg/urls.py +0 -33
  262. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/WHEEL +0 -0
  263. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/entry_points.txt +0 -0
  264. {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 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}")