django-cfg 1.3.7__py3-none-any.whl → 1.3.11__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 (246) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +269 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +183 -460
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +153 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
  48. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
  49. django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
  50. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
  51. django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
  52. django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
  53. django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
  54. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +43 -17
  55. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  56. django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
  57. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +109 -63
  58. django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
  59. django_cfg/apps/payments/config/__init__.py +14 -15
  60. django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
  61. django_cfg/apps/payments/config/helpers.py +8 -13
  62. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  63. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  64. django_cfg/apps/payments/middleware/api_access.py +32 -6
  65. django_cfg/apps/payments/migrations/0001_initial.py +33 -46
  66. django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
  67. django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
  68. django_cfg/apps/payments/models/balance.py +12 -0
  69. django_cfg/apps/payments/models/currencies.py +106 -32
  70. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  71. django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
  72. django_cfg/apps/payments/models/payments.py +94 -0
  73. django_cfg/apps/payments/services/core/base.py +4 -4
  74. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  75. django_cfg/apps/payments/services/core/payment_service.py +266 -39
  76. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  77. django_cfg/apps/payments/services/providers/base.py +303 -41
  78. django_cfg/apps/payments/services/providers/models/__init__.py +42 -0
  79. django_cfg/apps/payments/services/providers/models/base.py +145 -0
  80. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  81. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  82. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  83. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  84. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  85. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  86. django_cfg/apps/payments/services/providers/nowpayments/provider.py +557 -0
  87. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  88. django_cfg/apps/payments/services/providers/registry.py +9 -37
  89. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  90. django_cfg/apps/payments/services/types/requests.py +19 -7
  91. django_cfg/apps/payments/signals/payment_signals.py +31 -2
  92. django_cfg/apps/payments/static/payments/js/api-client.js +29 -6
  93. django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
  94. django_cfg/apps/payments/static/payments/js/payment-form.js +98 -32
  95. django_cfg/apps/payments/tasks/__init__.py +39 -0
  96. django_cfg/apps/payments/tasks/types.py +73 -0
  97. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  98. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  99. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  100. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  101. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  102. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  103. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  104. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  105. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  106. django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
  107. django_cfg/apps/payments/urls.py +3 -2
  108. django_cfg/apps/payments/urls_admin.py +1 -1
  109. django_cfg/apps/payments/views/api/currencies.py +8 -5
  110. django_cfg/apps/payments/views/overview/services.py +2 -2
  111. django_cfg/apps/payments/views/serializers/currencies.py +22 -8
  112. django_cfg/apps/support/admin/__init__.py +10 -1
  113. django_cfg/apps/support/admin/support_admin.py +338 -141
  114. django_cfg/apps/tasks/admin/__init__.py +11 -0
  115. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  116. django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
  117. django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
  118. django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
  119. django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
  120. django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
  121. django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
  122. django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
  123. django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
  124. django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
  125. django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
  126. django_cfg/apps/tasks/tasks/__init__.py +10 -0
  127. django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
  128. django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
  129. django_cfg/apps/tasks/templates/tasks/components/{status_cards.html → overview_content.html} +30 -11
  130. django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
  131. django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
  132. django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
  133. django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
  134. django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
  135. django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
  136. django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
  137. django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
  138. django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
  139. django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
  140. django_cfg/apps/tasks/urls.py +2 -2
  141. django_cfg/apps/tasks/urls_admin.py +2 -2
  142. django_cfg/apps/tasks/utils/__init__.py +1 -0
  143. django_cfg/apps/tasks/utils/simulator.py +356 -0
  144. django_cfg/apps/tasks/views/__init__.py +16 -0
  145. django_cfg/apps/tasks/views/api.py +569 -0
  146. django_cfg/apps/tasks/views/dashboard.py +58 -0
  147. django_cfg/config.py +1 -1
  148. django_cfg/core/config.py +10 -5
  149. django_cfg/core/generation.py +1 -1
  150. django_cfg/core/integration/__init__.py +21 -0
  151. django_cfg/management/commands/__init__.py +13 -1
  152. django_cfg/management/commands/migrate_all.py +9 -3
  153. django_cfg/management/commands/migrator.py +11 -6
  154. django_cfg/management/commands/rundramatiq.py +3 -2
  155. django_cfg/management/commands/rundramatiq_simulator.py +430 -0
  156. django_cfg/middleware/__init__.py +0 -2
  157. django_cfg/models/api_keys.py +115 -0
  158. django_cfg/models/constance.py +0 -11
  159. django_cfg/models/payments.py +137 -3
  160. django_cfg/modules/django_admin/__init__.py +64 -0
  161. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  162. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  163. django_cfg/modules/django_admin/decorators/display.py +106 -0
  164. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  165. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  166. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  167. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  168. django_cfg/modules/django_admin/models/__init__.py +20 -0
  169. django_cfg/modules/django_admin/models/action_models.py +33 -0
  170. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  171. django_cfg/modules/django_admin/models/base.py +26 -0
  172. django_cfg/modules/django_admin/models/display_models.py +31 -0
  173. django_cfg/modules/django_admin/utils/badges.py +159 -0
  174. django_cfg/modules/django_admin/utils/displays.py +247 -0
  175. django_cfg/modules/django_currency/__init__.py +2 -2
  176. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  177. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  178. django_cfg/modules/django_currency/core/converter.py +12 -12
  179. django_cfg/modules/django_currency/database/__init__.py +2 -2
  180. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  181. django_cfg/modules/django_llm/llm/client.py +10 -2
  182. django_cfg/modules/django_tasks.py +54 -21
  183. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  184. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  185. django_cfg/modules/django_unfold/dashboard.py +14 -13
  186. django_cfg/modules/django_unfold/models/config.py +1 -1
  187. django_cfg/registry/core.py +7 -9
  188. django_cfg/registry/third_party.py +2 -2
  189. django_cfg/template_archive/django_sample.zip +0 -0
  190. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -1
  191. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/RECORD +198 -160
  192. django_cfg/apps/accounts/admin/activity.py +0 -96
  193. django_cfg/apps/accounts/admin/group.py +0 -17
  194. django_cfg/apps/accounts/admin/otp.py +0 -59
  195. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  196. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  197. django_cfg/apps/accounts/admin/user.py +0 -300
  198. django_cfg/apps/agents/core/agent.py +0 -281
  199. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  200. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  201. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  202. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  203. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  204. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  205. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  206. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  207. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  208. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  209. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  210. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  211. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  212. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  213. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  214. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  215. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  216. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  217. django_cfg/apps/payments/config/constance/__init__.py +0 -22
  218. django_cfg/apps/payments/config/constance/config_service.py +0 -123
  219. django_cfg/apps/payments/config/constance/fields.py +0 -69
  220. django_cfg/apps/payments/config/constance/settings.py +0 -160
  221. django_cfg/apps/payments/services/providers/nowpayments.py +0 -478
  222. django_cfg/apps/tasks/admin.py +0 -320
  223. django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
  224. django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
  225. django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
  226. django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
  227. django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
  228. django_cfg/apps/tasks/templates/tasks/base.html +0 -96
  229. django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
  230. django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
  231. django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
  232. django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
  233. django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
  234. django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
  235. django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
  236. django_cfg/apps/tasks/views.py +0 -461
  237. django_cfg/management/commands/auto_generate.py +0 -486
  238. django_cfg/middleware/static_nocache.py +0 -55
  239. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  240. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  241. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  242. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  243. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  244. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,196 @@
1
+ """
2
+ NowPayments currency synchronization service.
3
+
4
+ Handles syncing currencies from NowPayments API to database.
5
+ """
6
+
7
+ from typing import Optional, Tuple, List
8
+ from django.db import transaction
9
+ from django.utils import timezone
10
+
11
+ from django_cfg.modules.django_logger import get_logger
12
+ from django_cfg.apps.payments.models import Currency, Network, ProviderCurrency
13
+ from ..models import UniversalCurrency, CurrencySyncResult
14
+ from .config import NowPaymentsConfig as Config
15
+
16
+ logger = get_logger("nowpayments_sync")
17
+
18
+
19
+ class NowPaymentsCurrencySync:
20
+ """Service for synchronizing NowPayments currencies to database."""
21
+
22
+ def __init__(self, provider_name: str = 'nowpayments'):
23
+ """Initialize currency sync service."""
24
+ self.provider_name = provider_name
25
+
26
+ def sync_currencies_to_db(self, currencies: List[UniversalCurrency]) -> CurrencySyncResult:
27
+ """
28
+ Sync universal currencies to database.
29
+
30
+ Args:
31
+ currencies: List of universal currencies from provider
32
+
33
+ Returns:
34
+ CurrencySyncResult: Sync operation results
35
+ """
36
+ result = CurrencySyncResult(total_processed=len(currencies))
37
+
38
+ try:
39
+ with transaction.atomic():
40
+ for currency in currencies:
41
+ try:
42
+ self._sync_single_currency(currency, result)
43
+ except Exception as e:
44
+ error_msg = f"Failed to sync {currency.provider_currency_code}: {e}"
45
+ logger.error(error_msg)
46
+ result.errors.append(error_msg)
47
+
48
+ logger.info(
49
+ f"Currency sync completed: {result.currencies_created} created, "
50
+ f"{result.currencies_updated} updated, "
51
+ f"{result.provider_currencies_created} provider currencies created, "
52
+ f"{len(result.errors)} errors"
53
+ )
54
+
55
+ return result
56
+
57
+ except Exception as e:
58
+ logger.error(f"Currency sync transaction failed: {e}")
59
+ result.errors.append(f"Transaction failed: {e}")
60
+ return result
61
+
62
+ def _sync_single_currency(self, currency: UniversalCurrency, result: CurrencySyncResult):
63
+ """Sync a single currency to database."""
64
+
65
+ # 1. Ensure base currency exists
66
+ base_currency, currency_created = self._get_or_create_currency(currency)
67
+ if currency_created:
68
+ result.currencies_created += 1
69
+ else:
70
+ # Update existing currency if needed
71
+ updated = self._update_currency_if_needed(base_currency, currency)
72
+ if updated:
73
+ result.currencies_updated += 1
74
+
75
+ # 2. Ensure network exists (if applicable)
76
+ network = None
77
+ if currency.network_code:
78
+ network, network_created = self._get_or_create_network(currency, base_currency)
79
+ if network_created:
80
+ result.networks_created += 1
81
+
82
+ # 3. Create or update provider currency
83
+ provider_currency, pc_created = self._get_or_create_provider_currency(
84
+ currency, base_currency, network
85
+ )
86
+
87
+ if pc_created:
88
+ result.provider_currencies_created += 1
89
+ else:
90
+ # Update existing provider currency
91
+ updated = self._update_provider_currency_if_needed(provider_currency, currency)
92
+ if updated:
93
+ result.provider_currencies_updated += 1
94
+
95
+ def _get_or_create_currency(self, currency: UniversalCurrency) -> Tuple[Currency, bool]:
96
+ """Get or create base currency."""
97
+
98
+ defaults = {
99
+ 'name': currency.name,
100
+ 'currency_type': currency.currency_type,
101
+ 'is_active': currency.is_enabled,
102
+ 'usd_rate': 1.0, # Will be updated by rate sync
103
+ 'usd_rate_updated_at': None,
104
+ 'decimal_places': self._get_decimal_places(currency),
105
+ }
106
+
107
+ return Currency.objects.get_or_create(
108
+ code=currency.base_currency_code,
109
+ defaults=defaults
110
+ )
111
+
112
+ def _update_currency_if_needed(self, base_currency: Currency, currency: UniversalCurrency) -> bool:
113
+ """Update currency if needed."""
114
+ updated = False
115
+
116
+ # Always update name to use the proper generated name
117
+ if base_currency.name != currency.name:
118
+ base_currency.name = currency.name
119
+ updated = True
120
+
121
+ # Update activity status
122
+ if base_currency.is_active != currency.is_enabled:
123
+ base_currency.is_active = currency.is_enabled
124
+ updated = True
125
+
126
+ if updated:
127
+ base_currency.save()
128
+
129
+ return updated
130
+
131
+ def _get_or_create_network(self, currency: UniversalCurrency, base_currency: Currency) -> Tuple[Network, bool]:
132
+ """Get or create network for currency."""
133
+
134
+ network_name = Config.get_network_name(currency.network_code)
135
+
136
+ defaults = {
137
+ 'name': network_name,
138
+ 'native_currency': base_currency,
139
+ 'is_active': True,
140
+ 'confirmation_blocks': Config.get_confirmation_blocks(currency.network_code),
141
+ }
142
+
143
+ return Network.objects.get_or_create(
144
+ code=currency.network_code,
145
+ defaults=defaults
146
+ )
147
+
148
+ def _get_or_create_provider_currency(
149
+ self,
150
+ currency: UniversalCurrency,
151
+ base_currency: Currency,
152
+ network: Optional[Network]
153
+ ) -> Tuple[ProviderCurrency, bool]:
154
+ """Get or create provider currency."""
155
+
156
+ defaults = {
157
+ 'currency': base_currency,
158
+ 'network': network,
159
+ 'is_enabled': currency.is_enabled,
160
+ }
161
+
162
+ return ProviderCurrency.objects.get_or_create(
163
+ provider=self.provider_name,
164
+ provider_currency_code=currency.provider_currency_code,
165
+ defaults=defaults
166
+ )
167
+
168
+ def _update_provider_currency_if_needed(
169
+ self,
170
+ provider_currency: ProviderCurrency,
171
+ currency: UniversalCurrency
172
+ ) -> bool:
173
+ """Update provider currency if needed."""
174
+ updated = False
175
+
176
+ # Update enabled status
177
+ if provider_currency.is_enabled != currency.is_enabled:
178
+ provider_currency.is_enabled = currency.is_enabled
179
+ updated = True
180
+
181
+ # Update timestamps
182
+ if updated:
183
+ provider_currency.updated_at = timezone.now()
184
+ provider_currency.save()
185
+
186
+ return updated
187
+
188
+ def _get_decimal_places(self, currency: UniversalCurrency) -> int:
189
+ """Get appropriate decimal places for currency."""
190
+ if currency.currency_type == 'fiat':
191
+ return 2
192
+ elif currency.is_stable:
193
+ return 6 # Stablecoins
194
+ else:
195
+ return 8 # Regular crypto
196
+
@@ -4,44 +4,16 @@ Provider registry for the Universal Payment System v2.0.
4
4
  Centralized management of payment providers with health monitoring and fallbacks.
5
5
  """
6
6
 
7
- from enum import Enum
8
7
  from typing import Dict, List, Optional, Type, Any
9
8
  from django_cfg.modules.django_logger import get_logger
10
9
  # ConfigService removed - using direct Constance access
11
10
  from ..types import ServiceOperationResult
12
- from .base import BaseProvider, ProviderConfig
13
- from .nowpayments import NowPaymentsProvider, NowPaymentsConfig
11
+ from .base import BaseProvider
12
+ from .models import ProviderConfig, ProviderEnum
13
+ from .nowpayments import NowPaymentsProvider, NowPaymentsProviderConfig
14
14
 
15
15
  logger = get_logger("provider_registry")
16
16
 
17
-
18
- # Provider enums
19
- class ProviderEnum(Enum):
20
- NOWPAYMENTS = "nowpayments"
21
- CRYPTAPI = "cryptapi"
22
- STRIPE = "stripe"
23
- CRYPTOMUS = "cryptomus"
24
-
25
- @classmethod
26
- def get_crypto_providers(cls):
27
- """Get list of crypto provider values."""
28
- return [cls.NOWPAYMENTS.value, cls.CRYPTAPI.value, cls.CRYPTOMUS.value]
29
-
30
- @classmethod
31
- def get_fiat_providers(cls):
32
- """Get list of fiat provider values."""
33
- return [cls.STRIPE.value]
34
-
35
- @classmethod
36
- def get_priority_order(cls):
37
- """Get list of provider values in priority order."""
38
- return [
39
- cls.NOWPAYMENTS.value,
40
- cls.CRYPTAPI.value,
41
- cls.STRIPE.value,
42
- cls.CRYPTOMUS.value
43
- ]
44
-
45
17
  class ProviderRegistry:
46
18
  """
47
19
  Registry for managing payment providers.
@@ -52,17 +24,17 @@ class ProviderRegistry:
52
24
 
53
25
  def __init__(self):
54
26
  """Initialize provider registry."""
55
- # Use PaymentConfigService for configuration
56
- from ...config.constance import get_payment_config_service
27
+ # Use PaymentsConfigManager for configuration from BaseCfgAutoModule
28
+ from ...config.django_cfg_integration import PaymentsConfigManager
57
29
 
58
- self.config_service = get_payment_config_service()
30
+ self.config_manager = PaymentsConfigManager
59
31
  self._providers: Dict[str, BaseProvider] = {}
60
32
 
61
33
  self._provider_classes: Dict[str, Type[BaseProvider]] = {
62
34
  ProviderEnum.NOWPAYMENTS.value: NowPaymentsProvider,
63
35
  }
64
36
  self._provider_configs: Dict[str, Type[ProviderConfig]] = {
65
- ProviderEnum.NOWPAYMENTS.value: NowPaymentsConfig,
37
+ ProviderEnum.NOWPAYMENTS.value: NowPaymentsProviderConfig,
66
38
  }
67
39
 
68
40
  self._health_status: Dict[str, bool] = {}
@@ -78,9 +50,9 @@ class ProviderRegistry:
78
50
  try:
79
51
  logger.info("Initializing provider registry")
80
52
 
81
- # Get all provider configurations
53
+ # Get all provider configurations from BaseCfgAutoModule
82
54
  try:
83
- provider_configs = self.config_service.get_all_provider_configs()
55
+ provider_configs = self.config_manager.get_all_provider_configs()
84
56
  except Exception as e:
85
57
  return ServiceOperationResult(
86
58
  success=False,
@@ -0,0 +1,277 @@
1
+ """
2
+ Universal provider synchronization service.
3
+
4
+ Handles synchronization of currencies from all payment providers to database.
5
+ """
6
+
7
+ from typing import List, Dict, Optional
8
+ from django.db import transaction
9
+ from django.utils import timezone
10
+ from datetime import timedelta
11
+
12
+ from django_cfg.modules.django_logger import get_logger
13
+ from django_cfg.apps.payments.models import ProviderCurrency
14
+ from .models import CurrencySyncResult
15
+ from .registry import get_provider_registry
16
+ from .base import BaseProvider
17
+
18
+ logger = get_logger("provider_sync")
19
+
20
+
21
+ class ProviderSyncService:
22
+ """Universal service for synchronizing all payment providers."""
23
+
24
+ def __init__(self):
25
+ """Initialize provider sync service."""
26
+ self.registry = get_provider_registry()
27
+
28
+ def sync_all_providers(
29
+ self,
30
+ force_refresh: bool = False,
31
+ provider_names: Optional[List[str]] = None
32
+ ) -> Dict[str, CurrencySyncResult]:
33
+ """
34
+ Sync currencies from all available providers.
35
+
36
+ Args:
37
+ force_refresh: Force refresh even if recently synced
38
+ provider_names: Specific providers to sync (None = all available)
39
+
40
+ Returns:
41
+ Dict[str, CurrencySyncResult]: Results by provider name
42
+ """
43
+ logger.info("Starting universal provider synchronization")
44
+
45
+ # Get providers to sync
46
+ if provider_names:
47
+ providers_to_sync = []
48
+ for name in provider_names:
49
+ provider = self.registry.get_provider(name)
50
+ if provider:
51
+ providers_to_sync.append((name, provider))
52
+ else:
53
+ logger.warning(f"Provider {name} not available")
54
+ else:
55
+ available_providers = self.registry.get_available_providers()
56
+ providers_to_sync = [
57
+ (name, self.registry.get_provider(name))
58
+ for name in available_providers
59
+ ]
60
+
61
+ if not providers_to_sync:
62
+ logger.warning("No providers available for synchronization")
63
+ return {}
64
+
65
+ # Sync each provider
66
+ results = {}
67
+ total_currencies = 0
68
+ total_errors = 0
69
+
70
+ for provider_name, provider in providers_to_sync:
71
+ try:
72
+ logger.info(f"Syncing provider: {provider_name}")
73
+
74
+ # Check if sync is needed
75
+ if not force_refresh and self._is_recently_synced(provider_name):
76
+ logger.info(f"Provider {provider_name} recently synced, skipping")
77
+ results[provider_name] = CurrencySyncResult(
78
+ total_processed=0,
79
+ errors=[f"Skipped - recently synced (use --force-refresh to override)"]
80
+ )
81
+ continue
82
+
83
+ # Perform sync
84
+ sync_result = self._sync_single_provider(provider)
85
+ results[provider_name] = sync_result
86
+
87
+ # Update stats
88
+ total_currencies += sync_result.currencies_created + sync_result.currencies_updated
89
+ total_errors += len(sync_result.errors)
90
+
91
+ # Mark sync time
92
+ self._mark_sync_time(provider_name)
93
+
94
+ logger.info(
95
+ f"Provider {provider_name} sync completed: "
96
+ f"{sync_result.currencies_created} created, "
97
+ f"{sync_result.currencies_updated} updated, "
98
+ f"{len(sync_result.errors)} errors"
99
+ )
100
+
101
+ except Exception as e:
102
+ error_msg = f"Provider {provider_name} sync failed: {e}"
103
+ logger.error(error_msg)
104
+ results[provider_name] = CurrencySyncResult(
105
+ total_processed=0,
106
+ errors=[error_msg]
107
+ )
108
+ total_errors += 1
109
+
110
+ logger.info(
111
+ f"Universal provider sync completed: "
112
+ f"{len(results)} providers processed, "
113
+ f"{total_currencies} currencies synced, "
114
+ f"{total_errors} errors"
115
+ )
116
+
117
+ return results
118
+
119
+ def sync_provider(
120
+ self,
121
+ provider_name: str,
122
+ force_refresh: bool = False
123
+ ) -> CurrencySyncResult:
124
+ """
125
+ Sync currencies from a specific provider.
126
+
127
+ Args:
128
+ provider_name: Name of provider to sync
129
+ force_refresh: Force refresh even if recently synced
130
+
131
+ Returns:
132
+ CurrencySyncResult: Sync operation result
133
+ """
134
+ provider = self.registry.get_provider(provider_name)
135
+ if not provider:
136
+ return CurrencySyncResult(
137
+ total_processed=0,
138
+ errors=[f"Provider {provider_name} not available"]
139
+ )
140
+
141
+ # Check if sync is needed
142
+ if not force_refresh and self._is_recently_synced(provider_name):
143
+ return CurrencySyncResult(
144
+ total_processed=0,
145
+ errors=[f"Provider {provider_name} recently synced (use force_refresh=True to override)"]
146
+ )
147
+
148
+ # Perform sync
149
+ result = self._sync_single_provider(provider)
150
+
151
+ # Mark sync time
152
+ self._mark_sync_time(provider_name)
153
+
154
+ return result
155
+
156
+ def get_sync_statistics(self) -> Dict[str, any]:
157
+ """
158
+ Get synchronization statistics for all providers.
159
+
160
+ Returns:
161
+ Dict: Statistics by provider
162
+ """
163
+ stats = {}
164
+
165
+ for provider_name in self.registry.list_providers():
166
+ provider_stats = self._get_provider_stats(provider_name)
167
+ stats[provider_name] = provider_stats
168
+
169
+ return stats
170
+
171
+ def _sync_single_provider(self, provider: BaseProvider) -> CurrencySyncResult:
172
+ """Sync currencies from a single provider."""
173
+ try:
174
+ logger.debug(f"Starting sync for provider: {provider.name}")
175
+
176
+ # Use provider's sync method
177
+ result = provider.sync_currencies_to_db()
178
+
179
+ logger.debug(
180
+ f"Provider {provider.name} sync result: "
181
+ f"{result.currencies_created} created, "
182
+ f"{result.currencies_updated} updated, "
183
+ f"{result.provider_currencies_created} provider currencies created"
184
+ )
185
+
186
+ return result
187
+
188
+ except Exception as e:
189
+ error_msg = f"Sync failed for provider {provider.name}: {e}"
190
+ logger.error(error_msg)
191
+ return CurrencySyncResult(
192
+ total_processed=0,
193
+ errors=[error_msg]
194
+ )
195
+
196
+ def _is_recently_synced(self, provider_name: str, hours: int = 1) -> bool:
197
+ """Check if provider was recently synced."""
198
+ try:
199
+ # Check if any provider currencies were updated recently
200
+ recent_threshold = timezone.now() - timedelta(hours=hours)
201
+
202
+ recent_updates = ProviderCurrency.objects.filter(
203
+ provider=provider_name,
204
+ updated_at__gte=recent_threshold
205
+ ).exists()
206
+
207
+ return recent_updates
208
+
209
+ except Exception as e:
210
+ logger.warning(f"Failed to check sync status for {provider_name}: {e}")
211
+ return False
212
+
213
+ def _mark_sync_time(self, provider_name: str):
214
+ """Mark sync time for provider (could be stored in cache/database)."""
215
+ # For now, we rely on ProviderCurrency.updated_at
216
+ # In future, could store in dedicated sync log table
217
+ pass
218
+
219
+ def _get_provider_stats(self, provider_name: str) -> Dict[str, any]:
220
+ """Get statistics for a specific provider."""
221
+ try:
222
+ from django.db.models import Count, Q
223
+
224
+ # Get provider currency stats
225
+ provider_currencies = ProviderCurrency.objects.filter(provider=provider_name)
226
+
227
+ total_currencies = provider_currencies.count()
228
+ enabled_currencies = provider_currencies.filter(is_enabled=True).count()
229
+
230
+ # Get recent activity
231
+ recent_threshold = timezone.now() - timedelta(hours=24)
232
+ recent_updates = provider_currencies.filter(
233
+ updated_at__gte=recent_threshold
234
+ ).count()
235
+
236
+ # Get last sync time
237
+ last_sync = None
238
+ if provider_currencies.exists():
239
+ last_sync = provider_currencies.order_by('-updated_at').first().updated_at
240
+
241
+ return {
242
+ 'total_currencies': total_currencies,
243
+ 'enabled_currencies': enabled_currencies,
244
+ 'disabled_currencies': total_currencies - enabled_currencies,
245
+ 'recent_updates_24h': recent_updates,
246
+ 'last_sync': last_sync.isoformat() if last_sync else None,
247
+ 'is_recently_synced': self._is_recently_synced(provider_name)
248
+ }
249
+
250
+ except Exception as e:
251
+ logger.error(f"Failed to get stats for {provider_name}: {e}")
252
+ return {
253
+ 'error': str(e),
254
+ 'total_currencies': 0,
255
+ 'enabled_currencies': 0,
256
+ 'disabled_currencies': 0,
257
+ 'recent_updates_24h': 0,
258
+ 'last_sync': None,
259
+ 'is_recently_synced': False
260
+ }
261
+
262
+
263
+ # Global sync service instance
264
+ _global_sync_service: Optional[ProviderSyncService] = None
265
+
266
+
267
+ def get_provider_sync_service() -> ProviderSyncService:
268
+ """
269
+ Get global provider sync service instance.
270
+
271
+ Returns:
272
+ ProviderSyncService: Global sync service instance
273
+ """
274
+ global _global_sync_service
275
+ if _global_sync_service is None:
276
+ _global_sync_service = ProviderSyncService()
277
+ return _global_sync_service
@@ -24,8 +24,8 @@ class PaymentCreateRequest(BaseModel):
24
24
 
25
25
  user_id: int = Field(gt=0, description="User ID")
26
26
  amount_usd: float = Field(gt=1.0, le=50000.0, description="Amount in USD")
27
- currency_code: Literal['BTC', 'ETH', 'LTC', 'XMR', 'USDT', 'USDC', 'ADA', 'DOT'] = Field(
28
- description="Cryptocurrency code"
27
+ currency_code: str = Field(
28
+ min_length=2, max_length=10, description="Cryptocurrency code"
29
29
  )
30
30
  provider: Literal['nowpayments'] = Field(default='nowpayments', description="Payment provider")
31
31
  callback_url: Optional[str] = Field(None, description="Success callback URL")
@@ -36,11 +36,23 @@ class PaymentCreateRequest(BaseModel):
36
36
  @field_validator('currency_code')
37
37
  @classmethod
38
38
  def validate_currency(cls, v: str) -> str:
39
- """Validate currency is supported."""
40
- supported = ['BTC', 'ETH', 'LTC', 'XMR', 'USDT', 'USDC', 'ADA', 'DOT']
41
- if v.upper() not in supported:
42
- raise ValueError(f"Currency {v} not supported. Supported: {', '.join(supported)}")
43
- return v.upper()
39
+ """Validate currency is supported by checking database."""
40
+ from django_cfg.apps.payments.models import Currency
41
+
42
+ currency_code = v.upper().strip()
43
+
44
+ # Check if currency exists in database and is active
45
+ try:
46
+ currency = Currency.objects.get(code=currency_code, is_active=True)
47
+ return currency_code
48
+ except Currency.DoesNotExist:
49
+ # Get list of active currencies for error message
50
+ active_currencies = Currency.objects.filter(is_active=True).values_list('code', flat=True)[:10]
51
+ currency_list = ', '.join(active_currencies) + ('...' if len(active_currencies) == 10 else '')
52
+ raise ValueError(f"Currency {currency_code} not found or inactive. Available: {currency_list}")
53
+ except Exception:
54
+ # Fallback validation if database is not available
55
+ return currency_code
44
56
 
45
57
  @field_validator('callback_url', 'cancel_url')
46
58
  @classmethod
@@ -17,16 +17,45 @@ logger = get_logger("payment_signals")
17
17
 
18
18
 
19
19
  @receiver(pre_save, sender=UniversalPayment)
20
- def store_original_status(sender, instance: UniversalPayment, **kwargs):
21
- """Store original status for change detection."""
20
+ def handle_status_changes(sender, instance: UniversalPayment, **kwargs):
21
+ """
22
+ Handle status changes and update status_changed_at field.
23
+
24
+ This signal automatically updates status_changed_at when status changes,
25
+ ensuring consistent tracking across all update methods.
26
+ """
22
27
  if instance.pk:
23
28
  try:
24
29
  original = UniversalPayment.objects.get(pk=instance.pk)
25
30
  instance._original_status = original.status
31
+
32
+ # Check if status has changed
33
+ if original.status != instance.status:
34
+ instance.status_changed_at = timezone.now()
35
+
36
+ # Set completed_at if status changed to completed
37
+ if instance.status == 'completed' and not instance.completed_at:
38
+ instance.completed_at = timezone.now()
39
+
40
+ logger.debug(f"Status change detected in pre_save", extra={
41
+ 'payment_id': str(instance.id),
42
+ 'old_status': original.status,
43
+ 'new_status': instance.status,
44
+ 'status_changed_at': instance.status_changed_at.isoformat()
45
+ })
26
46
  except UniversalPayment.DoesNotExist:
27
47
  instance._original_status = None
28
48
  else:
49
+ # New object - set status_changed_at if status is set
29
50
  instance._original_status = None
51
+ if instance.status and not instance.status_changed_at:
52
+ instance.status_changed_at = timezone.now()
53
+
54
+ logger.debug(f"New payment status set in pre_save", extra={
55
+ 'payment_id': 'new',
56
+ 'status': instance.status,
57
+ 'status_changed_at': instance.status_changed_at.isoformat()
58
+ })
30
59
 
31
60
 
32
61
  @receiver(post_save, sender=UniversalPayment)