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,557 @@
1
+ """
2
+ NowPayments provider implementation for Universal Payment System v2.0.
3
+
4
+ Enhanced crypto payment provider with currency synchronization.
5
+ """
6
+
7
+ import requests
8
+ import hashlib
9
+ import hmac
10
+ from typing import Optional, List, Dict, Any
11
+ from decimal import Decimal
12
+ from datetime import datetime
13
+
14
+ from django_cfg.modules.django_logger import get_logger
15
+ from ..base import BaseProvider
16
+ from ..models import PaymentRequest
17
+ from ...types import ProviderResponse, ServiceOperationResult
18
+ from .models import (
19
+ NowPaymentsProviderConfig,
20
+ NowPaymentsCurrency,
21
+ NowPaymentsFullCurrenciesResponse
22
+ )
23
+ from ..models import (
24
+ UniversalCurrency,
25
+ UniversalCurrenciesResponse,
26
+ CurrencySyncResult
27
+ )
28
+ from .sync import NowPaymentsCurrencySync
29
+ from .parsers import NowPaymentsCurrencyParser
30
+ from .config import NowPaymentsConfig as Config
31
+
32
+ logger = get_logger("nowpayments")
33
+
34
+
35
+ class NowPaymentsProvider(BaseProvider):
36
+ """NowPayments cryptocurrency payment provider."""
37
+
38
+ # Map NowPayments status to universal status
39
+ STATUS_MAPPING = {
40
+ 'waiting': 'pending',
41
+ 'confirming': 'processing',
42
+ 'confirmed': 'completed',
43
+ 'sending': 'processing',
44
+ 'partially_paid': 'pending',
45
+ 'finished': 'completed',
46
+ 'failed': 'failed',
47
+ 'refunded': 'refunded',
48
+ 'expired': 'expired'
49
+ }
50
+
51
+ def __init__(self, config: NowPaymentsProviderConfig):
52
+ """Initialize NowPayments provider."""
53
+ super().__init__(config)
54
+ self.config: NowPaymentsProviderConfig = config
55
+ self.sync_service = NowPaymentsCurrencySync(self.name)
56
+ self.parser = NowPaymentsCurrencyParser()
57
+
58
+ # Log initialization
59
+ api_key_str = str(self.config.api_key)
60
+ if hasattr(self.config.api_key, 'get_secret_value'):
61
+ api_key_str = self.config.api_key.get_secret_value()
62
+
63
+ logger.info(
64
+ f"🔑 NowPayments initialized: api_key={api_key_str[:10]}..., "
65
+ f"sandbox={self.is_sandbox}, base_url={self.config.api_url}"
66
+ )
67
+
68
+ # Override BaseProvider configuration methods
69
+ def get_fee_percentage(self, currency_code: str = None, currency_type: str = None) -> Decimal:
70
+ """Get NowPayments fee percentage."""
71
+ return Config.FEE_PERCENTAGE
72
+
73
+ def get_fixed_fee_usd(self, currency_code: str = None, currency_type: str = None) -> Decimal:
74
+ """Get NowPayments fixed fee."""
75
+ return Config.FIXED_FEE_USD
76
+
77
+ def get_min_amount_usd(self, currency_code: str = None, currency_type: str = None, is_stable: bool = False) -> Decimal:
78
+ """Get NowPayments minimum amount."""
79
+ return Config.get_min_amount()
80
+
81
+ def get_max_amount_usd(self, currency_code: str = None, currency_type: str = None) -> Decimal:
82
+ """Get NowPayments maximum amount."""
83
+ return Config.MAX_AMOUNT_USD
84
+
85
+ def get_confirmation_blocks(self, network_code: str) -> int:
86
+ """Get confirmation blocks for network."""
87
+ return Config.get_confirmation_blocks(network_code)
88
+
89
+ def get_network_name(self, network_code: str) -> str:
90
+ """Get human-readable network name."""
91
+ return Config.get_network_name(network_code)
92
+
93
+ def create_payment(self, request: PaymentRequest) -> ProviderResponse:
94
+ """Create payment with NowPayments."""
95
+ try:
96
+ self.logger.info("Creating NowPayments payment", extra={
97
+ 'amount_usd': request.amount_usd,
98
+ 'currency': request.currency_code,
99
+ 'order_id': request.order_id
100
+ })
101
+
102
+ # Use provider_currency_code from metadata if available, otherwise use original currency_code
103
+ provider_currency_code = request.metadata.get('provider_currency_code', request.currency_code)
104
+
105
+ # Prepare NowPayments request
106
+ payment_data = {
107
+ 'price_amount': request.amount_usd,
108
+ 'price_currency': 'USD',
109
+ 'pay_currency': provider_currency_code, # Use provider-specific currency code
110
+ 'order_id': request.order_id,
111
+ 'order_description': request.description or f'Payment {request.order_id}',
112
+ }
113
+
114
+ # Log the request data for debugging
115
+ self.logger.info("NowPayments request data", extra={
116
+ 'payment_data': payment_data,
117
+ 'original_currency_code': request.currency_code,
118
+ 'provider_currency_code': provider_currency_code,
119
+ 'request_amount_usd': request.amount_usd,
120
+ 'request_order_id': request.order_id
121
+ })
122
+
123
+ # Add optional fields
124
+ if request.callback_url:
125
+ payment_data['success_url'] = request.callback_url
126
+
127
+ if request.cancel_url:
128
+ payment_data['cancel_url'] = request.cancel_url
129
+
130
+ if request.customer_email:
131
+ payment_data['customer_email'] = request.customer_email
132
+
133
+ # Add IPN callback URL if configured
134
+ if self.config.callback_url:
135
+ payment_data['ipn_callback_url'] = self.config.callback_url
136
+
137
+ # Make API request
138
+ headers = {
139
+ 'x-api-key': self._get_api_key()
140
+ }
141
+
142
+ response_data = self._make_request(
143
+ method='POST',
144
+ endpoint='payment',
145
+ data=payment_data,
146
+ headers=headers
147
+ )
148
+
149
+ # Parse NowPayments response
150
+ if response_data and 'payment_id' in response_data:
151
+ # Log the full response for debugging
152
+ self.logger.info("NowPayments response received", extra={
153
+ 'payment_id': response_data.get('payment_id'),
154
+ 'pay_address': response_data.get('pay_address'),
155
+ 'pay_amount': response_data.get('pay_amount'),
156
+ 'full_response': response_data
157
+ })
158
+
159
+ # Successful payment creation
160
+ payment_url = response_data.get('invoice_url') or response_data.get('pay_url')
161
+
162
+ return self._create_provider_response(
163
+ success=True,
164
+ raw_response=response_data,
165
+ provider_payment_id=response_data['payment_id'],
166
+ status='waiting', # NowPayments initial status
167
+ amount=Decimal(str(response_data.get('pay_amount', 0))),
168
+ currency=request.currency_code,
169
+ payment_url=payment_url,
170
+ wallet_address=response_data.get('pay_address'),
171
+ expires_at=self._parse_expiry_time(response_data.get('expiration_estimate_date'))
172
+ )
173
+ else:
174
+ # Error response
175
+ error_message = response_data.get('message', 'Unknown error') if response_data else 'No response'
176
+ return self._create_provider_response(
177
+ success=False,
178
+ raw_response=response_data or {},
179
+ error_message=error_message
180
+ )
181
+
182
+ except Exception as e:
183
+ error_msg = str(e)
184
+ self.logger.error(f"NowPayments payment creation failed: {error_msg}", extra={
185
+ 'order_id': request.order_id,
186
+ 'error_type': type(e).__name__
187
+ })
188
+
189
+ # Provide user-friendly error messages
190
+ if "IP address blocked" in error_msg:
191
+ user_message = "NowPayments has blocked this IP address. Please contact support or try from a different location."
192
+ elif "Authentication failed" in error_msg:
193
+ user_message = "Invalid NowPayments API key. Please check your configuration."
194
+ elif "Bad request" in error_msg:
195
+ user_message = f"Invalid payment request: {error_msg}"
196
+ elif "Rate limit exceeded" in error_msg:
197
+ user_message = "Too many requests to NowPayments. Please try again in a few minutes."
198
+ elif "server error" in error_msg.lower():
199
+ user_message = "NowPayments service is temporarily unavailable. Please try again later."
200
+ else:
201
+ user_message = f"Payment creation failed: {error_msg}"
202
+
203
+ return self._create_provider_response(
204
+ success=False,
205
+ raw_response={'error': error_msg, 'error_type': type(e).__name__},
206
+ error_message=user_message
207
+ )
208
+
209
+ def get_payment_status(self, provider_payment_id: str) -> ProviderResponse:
210
+ """Get payment status from NowPayments."""
211
+ try:
212
+ self.logger.debug("Getting NowPayments payment status", extra={
213
+ 'payment_id': provider_payment_id
214
+ })
215
+
216
+ headers = {
217
+ 'x-api-key': self._get_api_key()
218
+ }
219
+
220
+ response_data = self._make_request(
221
+ method='GET',
222
+ endpoint=f'payment/{provider_payment_id}',
223
+ headers=headers
224
+ )
225
+
226
+ if response_data and 'payment_status' in response_data:
227
+ provider_status = response_data['payment_status']
228
+ universal_status = self.STATUS_MAPPING.get(provider_status, 'unknown')
229
+
230
+ return self._create_provider_response(
231
+ success=True,
232
+ raw_response=response_data,
233
+ provider_payment_id=provider_payment_id,
234
+ status=universal_status,
235
+ amount=Decimal(str(response_data.get('pay_amount', 0))),
236
+ currency=response_data.get('pay_currency'),
237
+ wallet_address=response_data.get('pay_address')
238
+ )
239
+ else:
240
+ error_message = response_data.get('message', 'Payment not found') if response_data else 'No response'
241
+ return self._create_provider_response(
242
+ success=False,
243
+ raw_response=response_data or {},
244
+ error_message=error_message
245
+ )
246
+
247
+ except Exception as e:
248
+ error_msg = str(e)
249
+ self.logger.error(f"NowPayments status check failed: {error_msg}", extra={
250
+ 'payment_id': provider_payment_id,
251
+ 'error_type': type(e).__name__
252
+ })
253
+
254
+ # Provide user-friendly error messages
255
+ if "IP address blocked" in error_msg:
256
+ user_message = "NowPayments has blocked this IP address. Cannot check payment status."
257
+ elif "Authentication failed" in error_msg:
258
+ user_message = "Invalid NowPayments API key. Cannot check payment status."
259
+ elif "server error" in error_msg.lower():
260
+ user_message = "NowPayments service is temporarily unavailable. Please try again later."
261
+ else:
262
+ user_message = f"Status check failed: {error_msg}"
263
+
264
+ return self._create_provider_response(
265
+ success=False,
266
+ raw_response={'error': error_msg, 'error_type': type(e).__name__},
267
+ error_message=user_message
268
+ )
269
+
270
+ def get_supported_currencies(self) -> ServiceOperationResult:
271
+ """Get supported currencies from NowPayments."""
272
+ try:
273
+ self.logger.debug("Getting NowPayments supported currencies")
274
+
275
+ headers = {
276
+ 'x-api-key': self._get_api_key()
277
+ }
278
+
279
+ response_data = self._make_request(
280
+ method='GET',
281
+ endpoint='full-currencies',
282
+ headers=headers
283
+ )
284
+
285
+ if response_data and 'currencies' in response_data:
286
+ currencies = response_data['currencies']
287
+
288
+ return ServiceOperationResult(
289
+ success=True,
290
+ message=f"Retrieved {len(currencies)} supported currencies",
291
+ data={
292
+ 'currencies': currencies,
293
+ 'count': len(currencies),
294
+ 'provider': self.name
295
+ }
296
+ )
297
+ else:
298
+ return ServiceOperationResult(
299
+ success=False,
300
+ message="Failed to get currencies from NowPayments",
301
+ error_code="currencies_fetch_failed"
302
+ )
303
+
304
+ except Exception as e:
305
+ self.logger.error(f"NowPayments currencies fetch failed: {e}")
306
+
307
+ return ServiceOperationResult(
308
+ success=False,
309
+ message=f"Currencies fetch error: {e}",
310
+ error_code="currencies_fetch_error"
311
+ )
312
+
313
+ def get_parsed_currencies(self) -> UniversalCurrenciesResponse:
314
+ """Get parsed and normalized currencies from NowPayments."""
315
+ try:
316
+ # Use full-currencies endpoint to get detailed currency info
317
+ headers = {
318
+ 'x-api-key': self._get_api_key()
319
+ }
320
+
321
+ response_data = self._make_request(
322
+ method='GET',
323
+ endpoint='full-currencies',
324
+ headers=headers
325
+ )
326
+
327
+ if not response_data or 'currencies' not in response_data:
328
+ return UniversalCurrenciesResponse(currencies=[])
329
+
330
+ universal_currencies = []
331
+
332
+ for currency_data in response_data['currencies']:
333
+ if not currency_data.get('enable', True):
334
+ continue # Skip disabled currencies
335
+
336
+ provider_code = currency_data.get('code', '').upper()
337
+ if not provider_code:
338
+ continue
339
+
340
+ # Parse provider code into base currency + network using API data
341
+ currency_name = currency_data.get('name', '')
342
+ api_network = currency_data.get('network')
343
+ ticker = currency_data.get('ticker', '')
344
+
345
+ # Use parser to extract base currency and network
346
+ parse_result = self.parser.parse_currency_code(
347
+ provider_code, currency_name, api_network, ticker
348
+ )
349
+
350
+ # Skip currencies that should be filtered out (empty network duplicates)
351
+ if parse_result[0] is None:
352
+ continue
353
+
354
+ base_currency_code, network_code = parse_result
355
+
356
+ # Determine currency type
357
+ currency_type = 'fiat' if network_code is None else 'crypto'
358
+
359
+ # Generate proper currency name
360
+ proper_name = self.parser.generate_currency_name(
361
+ base_currency_code, network_code, currency_name
362
+ )
363
+
364
+ universal_currency = UniversalCurrency(
365
+ provider_currency_code=provider_code,
366
+ base_currency_code=base_currency_code,
367
+ network_code=network_code,
368
+ name=proper_name, # Use generated name instead of API name
369
+ currency_type=currency_type,
370
+ is_enabled=currency_data.get('enable', True),
371
+ is_popular=currency_data.get('is_popular', False),
372
+ is_stable=currency_data.get('is_stable', False),
373
+ priority=currency_data.get('priority', 0),
374
+ logo_url=currency_data.get('logo_url', ''),
375
+ available_for_payment=currency_data.get('available_for_payment', True),
376
+ available_for_payout=currency_data.get('available_for_payout', True),
377
+ raw_data=currency_data
378
+ )
379
+
380
+ universal_currencies.append(universal_currency)
381
+
382
+ return UniversalCurrenciesResponse(currencies=universal_currencies)
383
+
384
+ except Exception as e:
385
+ logger.error(f"Error parsing currencies: {e}")
386
+ return UniversalCurrenciesResponse(currencies=[])
387
+
388
+ def sync_currencies_to_db(self) -> CurrencySyncResult:
389
+ """Sync currencies from NowPayments API to database."""
390
+ try:
391
+ self.logger.info("Starting NowPayments currency synchronization")
392
+
393
+ # Get parsed currencies from API
394
+ currencies_response = self.get_parsed_currencies()
395
+
396
+ if not currencies_response.currencies:
397
+ return CurrencySyncResult(
398
+ errors=["No currencies received from NowPayments API"]
399
+ )
400
+
401
+ # Sync to database
402
+ result = self.sync_service.sync_currencies_to_db(currencies_response.currencies)
403
+
404
+ self.logger.info(
405
+ f"NowPayments currency sync completed: "
406
+ f"{result.currencies_created} currencies created, "
407
+ f"{result.provider_currencies_created} provider currencies created, "
408
+ f"{len(result.errors)} errors"
409
+ )
410
+
411
+ return result
412
+
413
+ except Exception as e:
414
+ error_msg = f"Currency sync failed: {e}"
415
+ self.logger.error(error_msg)
416
+ return CurrencySyncResult(errors=[error_msg])
417
+
418
+ def validate_webhook(self, payload: Dict[str, Any], signature: str = None) -> ServiceOperationResult:
419
+ """Validate NowPayments IPN webhook."""
420
+ try:
421
+ self.logger.debug("Validating NowPayments webhook", extra={
422
+ 'has_signature': bool(signature),
423
+ 'payment_id': payload.get('payment_id')
424
+ })
425
+
426
+ # Validate payload structure
427
+ try:
428
+ from .models import NowPaymentsWebhook
429
+ webhook_data = NowPaymentsWebhook(**payload)
430
+ except Exception as e:
431
+ return ServiceOperationResult(
432
+ success=False,
433
+ message=f"Invalid webhook payload: {e}",
434
+ error_code="invalid_payload"
435
+ )
436
+
437
+ # Validate signature if provided and secret is configured
438
+ if signature and self.config.ipn_secret:
439
+ is_valid_signature = self._validate_ipn_signature(payload, signature)
440
+ if not is_valid_signature:
441
+ return ServiceOperationResult(
442
+ success=False,
443
+ message="Invalid webhook signature",
444
+ error_code="invalid_signature"
445
+ )
446
+
447
+ return ServiceOperationResult(
448
+ success=True,
449
+ message="Webhook validated successfully",
450
+ data={
451
+ 'provider': self.name,
452
+ 'payment_id': webhook_data.payment_id,
453
+ 'status': webhook_data.payment_status,
454
+ 'signature_validated': bool(signature and self.config.ipn_secret),
455
+ 'webhook_data': webhook_data.model_dump()
456
+ }
457
+ )
458
+
459
+ except Exception as e:
460
+ self.logger.error(f"NowPayments webhook validation failed: {e}")
461
+
462
+ return ServiceOperationResult(
463
+ success=False,
464
+ message=f"Webhook validation error: {e}",
465
+ error_code="validation_error"
466
+ )
467
+
468
+ def health_check(self) -> ServiceOperationResult:
469
+ """Perform NowPayments-specific health check."""
470
+ try:
471
+ # Test API connectivity by getting status
472
+ headers = {
473
+ 'x-api-key': self._get_api_key()
474
+ }
475
+
476
+ response_data = self._make_request(
477
+ method='GET',
478
+ endpoint='status',
479
+ headers=headers
480
+ )
481
+
482
+ if response_data and response_data.get('message') == 'OK':
483
+ # Also check currencies endpoint
484
+ currencies_result = self.get_supported_currencies()
485
+ currency_count = len(currencies_result.data.get('currencies', [])) if currencies_result.success else 0
486
+
487
+ return ServiceOperationResult(
488
+ success=True,
489
+ message="NowPayments provider is healthy",
490
+ data={
491
+ 'provider': self.name,
492
+ 'sandbox': self.is_sandbox,
493
+ 'api_url': self.config.api_url,
494
+ 'supported_currencies': currency_count,
495
+ 'has_ipn_secret': bool(self.config.ipn_secret),
496
+ 'api_key_configured': bool(self.config.api_key)
497
+ }
498
+ )
499
+ else:
500
+ return ServiceOperationResult(
501
+ success=False,
502
+ message="NowPayments API connectivity failed",
503
+ error_code="api_connectivity_failed",
504
+ data={
505
+ 'provider': self.name,
506
+ 'response': response_data
507
+ }
508
+ )
509
+
510
+ except Exception as e:
511
+ return ServiceOperationResult(
512
+ success=False,
513
+ message=f"NowPayments health check error: {e}",
514
+ error_code="health_check_error",
515
+ data={'provider': self.name}
516
+ )
517
+
518
+
519
+ def _validate_ipn_signature(self, payload: Dict[str, Any], signature: str) -> bool:
520
+ """Validate IPN signature using HMAC-SHA512."""
521
+ try:
522
+ import json
523
+
524
+ # Sort payload and create canonical string
525
+ sorted_payload = json.dumps(payload, separators=(',', ':'), sort_keys=True)
526
+
527
+ # Calculate expected signature
528
+ expected_signature = hmac.new(
529
+ self.config.ipn_secret.encode('utf-8'),
530
+ sorted_payload.encode('utf-8'),
531
+ hashlib.sha512
532
+ ).hexdigest()
533
+
534
+ # Compare signatures
535
+ return hmac.compare_digest(expected_signature, signature)
536
+
537
+ except Exception as e:
538
+ self.logger.error(f"Signature validation error: {e}")
539
+ return False
540
+
541
+ def _parse_expiry_time(self, expiry_str: Optional[str]) -> Optional[datetime]:
542
+ """Parse NowPayments expiry time string."""
543
+ if not expiry_str:
544
+ return None
545
+
546
+ try:
547
+ # NowPayments typically returns ISO format
548
+ return datetime.fromisoformat(expiry_str.replace('Z', '+00:00'))
549
+ except Exception:
550
+ self.logger.warning(f"Failed to parse expiry time: {expiry_str}")
551
+ return None
552
+
553
+ def _get_api_key(self) -> str:
554
+ """Get API key as string."""
555
+ if hasattr(self.config.api_key, 'get_secret_value'):
556
+ return self.config.api_key.get_secret_value()
557
+ return str(self.config.api_key)