django-cfg 1.2.29__py3-none-any.whl → 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) 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 -9
  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 +600 -108
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +470 -64
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
  39. django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
  43. django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +16 -20
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +207 -67
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -284
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +344 -468
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -484
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +232 -71
  74. django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
  75. django_cfg/apps/payments/services/providers/registry.py +429 -80
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +211 -130
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +129 -98
  85. django_cfg/apps/payments/signals/subscription_signals.py +195 -143
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +46 -47
  93. django_cfg/apps/payments/urls_admin.py +49 -0
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/apps/tasks/urls.py +0 -2
  110. django_cfg/apps/tasks/urls_admin.py +14 -0
  111. django_cfg/apps/urls.py +4 -4
  112. django_cfg/config.py +1 -1
  113. django_cfg/core/config.py +75 -4
  114. django_cfg/core/generation.py +25 -4
  115. django_cfg/core/integration/README.md +363 -0
  116. django_cfg/core/integration/__init__.py +47 -0
  117. django_cfg/core/integration/commands_collector.py +239 -0
  118. django_cfg/core/integration/display/__init__.py +15 -0
  119. django_cfg/core/integration/display/base.py +157 -0
  120. django_cfg/core/integration/display/ngrok.py +164 -0
  121. django_cfg/core/integration/display/startup.py +815 -0
  122. django_cfg/core/integration/url_integration.py +123 -0
  123. django_cfg/core/integration/version_checker.py +160 -0
  124. django_cfg/management/commands/auto_generate.py +4 -0
  125. django_cfg/management/commands/check_settings.py +6 -0
  126. django_cfg/management/commands/clear_constance.py +5 -2
  127. django_cfg/management/commands/create_token.py +6 -0
  128. django_cfg/management/commands/list_urls.py +6 -0
  129. django_cfg/management/commands/migrate_all.py +6 -0
  130. django_cfg/management/commands/migrator.py +3 -0
  131. django_cfg/management/commands/rundramatiq.py +6 -0
  132. django_cfg/management/commands/runserver_ngrok.py +51 -29
  133. django_cfg/management/commands/script.py +6 -0
  134. django_cfg/management/commands/show_config.py +12 -2
  135. django_cfg/management/commands/show_urls.py +4 -0
  136. django_cfg/management/commands/superuser.py +6 -0
  137. django_cfg/management/commands/task_clear.py +4 -1
  138. django_cfg/management/commands/task_status.py +3 -1
  139. django_cfg/management/commands/test_email.py +3 -0
  140. django_cfg/management/commands/test_telegram.py +6 -0
  141. django_cfg/management/commands/test_twilio.py +6 -0
  142. django_cfg/management/commands/tree.py +6 -0
  143. django_cfg/management/commands/validate_config.py +155 -149
  144. django_cfg/models/constance.py +31 -11
  145. django_cfg/models/payments.py +175 -498
  146. django_cfg/modules/django_currency/__init__.py +16 -11
  147. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  148. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  149. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  150. django_cfg/modules/django_currency/core/__init__.py +1 -7
  151. django_cfg/modules/django_currency/core/converter.py +18 -23
  152. django_cfg/modules/django_currency/core/models.py +122 -11
  153. django_cfg/modules/django_currency/database/__init__.py +4 -4
  154. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  155. django_cfg/modules/django_logger.py +160 -146
  156. django_cfg/modules/django_unfold/dashboard.py +65 -12
  157. django_cfg/registry/core.py +1 -0
  158. django_cfg/template_archive/django_sample.zip +0 -0
  159. django_cfg/templates/admin/components/action_grid.html +9 -9
  160. django_cfg/templates/admin/components/metric_card.html +5 -5
  161. django_cfg/templates/admin/components/status_badge.html +2 -2
  162. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  163. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  164. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  165. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  166. django_cfg/utils/smart_defaults.py +222 -571
  167. django_cfg/utils/toolkit.py +51 -11
  168. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
  169. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
  170. django_cfg/apps/payments/__init__.py +0 -8
  171. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  172. django_cfg/apps/payments/config/module.py +0 -70
  173. django_cfg/apps/payments/config/providers.py +0 -105
  174. django_cfg/apps/payments/config/settings.py +0 -96
  175. django_cfg/apps/payments/config/utils.py +0 -52
  176. django_cfg/apps/payments/decorators.py +0 -291
  177. django_cfg/apps/payments/management/commands/README.md +0 -178
  178. django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
  179. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  180. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  181. django_cfg/apps/payments/managers/__init__.py +0 -22
  182. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  183. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  184. django_cfg/apps/payments/managers/currency_manager.py +0 -83
  185. django_cfg/apps/payments/managers/payment_manager.py +0 -44
  186. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  187. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  188. django_cfg/apps/payments/models/events.py +0 -73
  189. django_cfg/apps/payments/serializers/__init__.py +0 -56
  190. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  191. django_cfg/apps/payments/serializers/balance.py +0 -59
  192. django_cfg/apps/payments/serializers/currencies.py +0 -55
  193. django_cfg/apps/payments/serializers/payments.py +0 -62
  194. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  195. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  196. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  197. django_cfg/apps/payments/services/cache/base.py +0 -30
  198. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  199. django_cfg/apps/payments/services/internal_types.py +0 -297
  200. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  201. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  202. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
  203. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  204. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  205. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  206. django_cfg/apps/payments/services/security/__init__.py +0 -34
  207. django_cfg/apps/payments/services/security/error_handler.py +0 -637
  208. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  209. django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
  210. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  211. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  212. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  213. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  214. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  215. django_cfg/apps/payments/tasks/__init__.py +0 -12
  216. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  217. django_cfg/apps/payments/templates/payments/base.html +0 -182
  218. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  219. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  220. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
  221. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  222. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
  223. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
  224. django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
  225. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  226. django_cfg/apps/payments/urls_templates.py +0 -52
  227. django_cfg/apps/payments/utils/__init__.py +0 -45
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -245
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -62
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -111
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -312
  241. django_cfg/apps/payments/views/templates/base.py +0 -204
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -164
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -240
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -65
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  252. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  253. django_cfg/template_archive/.gitignore +0 -1
  254. django_cfg/template_archive/__init__.py +0 -0
  255. django_cfg/urls.py +0 -33
  256. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  257. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  258. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,293 +1,478 @@
1
1
  """
2
- NowPayments provider implementation.
2
+ NowPayments provider for the Universal Payment System v2.0.
3
3
 
4
- Enhanced crypto payment provider with minimal typing.
4
+ Implementation of NowPayments API integration with unified interface.
5
5
  """
6
6
 
7
- import logging
8
- import requests
9
- import hashlib
10
- import hmac
11
- from typing import Optional, List
7
+ from typing import Dict, Any, Optional, List
12
8
  from decimal import Decimal
9
+ from datetime import datetime
13
10
  from pydantic import BaseModel, Field
11
+ import hmac
12
+ import hashlib
13
+ import json
14
14
 
15
- from .base import PaymentProvider
16
- from ..internal_types import ProviderResponse, WebhookData
17
-
18
- logger = logging.getLogger(__name__)
15
+ from .base import BaseProvider, ProviderConfig, PaymentRequest
16
+ from ..types import ProviderResponse, ServiceOperationResult, NowPaymentsWebhook
17
+ from django_cfg.modules.django_currency import convert_currency
19
18
 
20
19
 
21
- class NowPaymentsConfig(BaseModel):
22
- """NowPayments provider configuration."""
23
- api_key: str = Field(..., description="NowPayments API key")
24
- sandbox: bool = Field(default=False, description="Use sandbox mode")
25
- ipn_secret: Optional[str] = Field(default=None, description="IPN secret for webhook validation")
26
- callback_url: Optional[str] = Field(default=None, description="Webhook callback URL")
27
- success_url: Optional[str] = Field(default=None, description="Payment success redirect URL")
28
- cancel_url: Optional[str] = Field(default=None, description="Payment cancel redirect URL")
29
- enabled: bool = Field(default=True, description="Provider enabled")
20
+ class NowPaymentsConfig(ProviderConfig):
21
+ """
22
+ NowPayments-specific configuration.
23
+
24
+ Extends base config with NowPayments-specific fields.
25
+ """
26
+
27
+ ipn_secret: Optional[str] = Field(None, description="IPN callback secret")
28
+
29
+ def __init__(self, **data):
30
+ """Initialize with NowPayments defaults."""
31
+ # Set NowPayments-specific defaults
32
+ if 'provider_name' not in data:
33
+ data['provider_name'] = 'nowpayments'
34
+
35
+ if 'api_url' not in data:
36
+ sandbox = data.get('sandbox', False)
37
+ data['api_url'] = (
38
+ 'https://api-sandbox.nowpayments.io/v1' if sandbox
39
+ else 'https://api.nowpayments.io/v1'
40
+ )
41
+
42
+ if 'supported_currencies' not in data:
43
+ data['supported_currencies'] = [
44
+ 'BTC', 'ETH', 'LTC', 'XMR', 'USDT', 'USDC', 'ADA', 'DOT'
45
+ ]
46
+
47
+ super().__init__(**data)
30
48
 
31
49
 
32
- class NowPaymentsProvider(PaymentProvider):
33
- """NowPayments cryptocurrency payment provider."""
50
+ class NowPaymentsProvider(BaseProvider):
51
+ """
52
+ NowPayments provider implementation.
53
+
54
+ Handles payment creation, status checking, and webhook validation for NowPayments.
55
+ """
34
56
 
35
57
  def __init__(self, config: NowPaymentsConfig):
36
58
  """Initialize NowPayments provider."""
37
- super().__init__(config.dict())
38
- self.config = config
39
- self.api_key = config.api_key
40
- self.sandbox = config.sandbox
41
- self.ipn_secret = config.ipn_secret or ''
42
- self.base_url = self._get_base_url()
43
-
44
- # Configurable URLs
45
- self.callback_url = config.callback_url
46
- self.success_url = config.success_url
47
- self.cancel_url = config.cancel_url
48
-
49
- self.headers = {
50
- 'x-api-key': self.api_key,
51
- 'Content-Type': 'application/json'
52
- }
59
+ super().__init__(config)
60
+ self.config: NowPaymentsConfig = config
53
61
 
54
- def _get_base_url(self) -> str:
55
- """Get base URL based on sandbox mode."""
56
- if self.sandbox:
57
- return 'https://api-sandbox.nowpayments.io/v1'
58
- return 'https://api.nowpayments.io/v1'
59
-
60
- def _make_request(self, method: str, endpoint: str, data: Optional[dict] = None) -> Optional[dict]:
61
- """Make HTTP request to NowPayments API with error handling."""
62
+ def create_payment(self, request: PaymentRequest) -> ProviderResponse:
63
+ """
64
+ Create payment with NowPayments.
65
+
66
+ Args:
67
+ request: Payment creation request
68
+
69
+ Returns:
70
+ ProviderResponse: NowPayments response
71
+ """
62
72
  try:
63
- url = f"{self.base_url}/{endpoint}"
64
-
65
- response = requests.request(
66
- method=method,
67
- url=url,
68
- headers=self.headers,
69
- json=data,
70
- timeout=30
71
- )
73
+ self.logger.info("Creating NowPayments payment", extra={
74
+ 'amount_usd': request.amount_usd,
75
+ 'currency': request.currency_code,
76
+ 'order_id': request.order_id
77
+ })
72
78
 
73
- response.raise_for_status()
74
- return response.json()
79
+ # Convert USD to crypto amount
80
+ try:
81
+ crypto_amount = convert_currency(
82
+ request.amount_usd,
83
+ 'USD',
84
+ request.currency_code
85
+ )
86
+ except Exception as e:
87
+ return self._create_provider_response(
88
+ success=False,
89
+ raw_response={'error': f'Currency conversion failed: {e}'},
90
+ error_message=f'Currency conversion failed: {e}'
91
+ )
75
92
 
76
- except requests.exceptions.RequestException as e:
77
- logger.error(f"NowPayments API request failed: {e}")
78
- return None
79
- except Exception as e:
80
- logger.error(f"Unexpected error in NowPayments request: {e}")
81
- return None
82
-
83
- def create_payment(self, payment_data: dict) -> ProviderResponse:
84
- """Create payment via NowPayments API."""
85
- try:
86
- amount = Decimal(str(payment_data['amount']))
87
- currency = payment_data['currency']
88
- order_id = payment_data.get('order_id', f'payment_{int(amount * 100)}_{currency}')
89
-
90
- payment_request = {
91
- 'price_amount': float(amount),
92
- 'price_currency': 'usd', # Base currency
93
- 'pay_currency': currency,
94
- 'order_id': order_id,
95
- 'order_description': payment_data.get('description', f'Payment {order_id}'),
93
+ # Prepare NowPayments request
94
+ payment_data = {
95
+ 'price_amount': request.amount_usd,
96
+ 'price_currency': 'USD',
97
+ 'pay_currency': request.currency_code,
98
+ 'order_id': request.order_id,
99
+ 'order_description': request.description or f'Payment {request.order_id}',
96
100
  }
97
101
 
98
- # Add optional URLs
99
- if self.success_url:
100
- payment_request['success_url'] = self.success_url
101
- if self.cancel_url:
102
- payment_request['cancel_url'] = self.cancel_url
103
- if self.callback_url:
104
- payment_request['ipn_callback_url'] = self.callback_url
102
+ # Add optional fields
103
+ if request.callback_url:
104
+ payment_data['success_url'] = request.callback_url
105
+
106
+ if request.cancel_url:
107
+ payment_data['cancel_url'] = request.cancel_url
108
+
109
+ if request.customer_email:
110
+ payment_data['customer_email'] = request.customer_email
105
111
 
106
- response = self._make_request('POST', 'payment', payment_request)
112
+ # Add IPN callback URL (would be configured via webhook service)
113
+ if hasattr(self, '_ipn_callback_url'):
114
+ payment_data['ipn_callback_url'] = self._ipn_callback_url
107
115
 
108
- if response:
109
- return ProviderResponse(
116
+ # Make API request
117
+ headers = {
118
+ 'x-api-key': self.config.api_key
119
+ }
120
+
121
+ response_data = self._make_request(
122
+ method='POST',
123
+ endpoint='payment',
124
+ data=payment_data,
125
+ headers=headers
126
+ )
127
+
128
+ # Parse NowPayments response
129
+ if 'payment_id' in response_data:
130
+ # Successful payment creation
131
+ payment_url = response_data.get('invoice_url') or response_data.get('pay_url')
132
+
133
+ return self._create_provider_response(
110
134
  success=True,
111
- provider_payment_id=response.get('payment_id'),
112
- payment_url=response.get('invoice_url'),
113
- pay_address=response.get('pay_address'),
114
- amount=Decimal(str(response.get('pay_amount', 0))),
115
- currency=response.get('pay_currency'),
116
- status='pending'
135
+ raw_response=response_data,
136
+ provider_payment_id=response_data['payment_id'],
137
+ status='waiting', # NowPayments initial status
138
+ amount=Decimal(str(crypto_amount)),
139
+ currency=request.currency_code,
140
+ payment_url=payment_url,
141
+ wallet_address=response_data.get('pay_address'),
142
+ qr_code_url=response_data.get('qr_code_url'),
143
+ expires_at=self._parse_expiry_time(response_data.get('expiration_estimate_date'))
117
144
  )
118
145
  else:
119
- return ProviderResponse(
146
+ # Error response
147
+ error_message = response_data.get('message', 'Unknown error')
148
+ return self._create_provider_response(
120
149
  success=False,
121
- error_message='Failed to create payment'
150
+ raw_response=response_data,
151
+ error_message=error_message
122
152
  )
123
153
 
124
154
  except Exception as e:
125
- logger.error(f"NowPayments create_payment error: {e}")
126
- return ProviderResponse(
155
+ self.logger.error(f"NowPayments payment creation failed: {e}", extra={
156
+ 'order_id': request.order_id
157
+ })
158
+
159
+ return self._create_provider_response(
127
160
  success=False,
128
- error_message=str(e)
161
+ raw_response={'error': str(e)},
162
+ error_message=f'Payment creation failed: {e}'
129
163
  )
130
164
 
131
- def check_payment_status(self, payment_id: str) -> ProviderResponse:
132
- """Check payment status via NowPayments API."""
165
+ def get_payment_status(self, provider_payment_id: str) -> ProviderResponse:
166
+ """
167
+ Get payment status from NowPayments.
168
+
169
+ Args:
170
+ provider_payment_id: NowPayments payment ID
171
+
172
+ Returns:
173
+ ProviderResponse: Current payment status
174
+ """
133
175
  try:
134
- response = self._make_request('GET', f'payment/{payment_id}')
135
-
136
- if response:
137
- # Map NowPayments status to universal status
138
- status_mapping = {
139
- 'waiting': 'pending',
140
- 'confirming': 'processing',
141
- 'confirmed': 'completed',
142
- 'sending': 'processing',
143
- 'partially_paid': 'pending',
144
- 'finished': 'completed',
145
- 'failed': 'failed',
146
- 'refunded': 'refunded',
147
- 'expired': 'expired'
148
- }
149
-
150
- provider_status = response.get('payment_status', 'unknown')
151
- universal_status = status_mapping.get(provider_status, 'unknown')
152
-
153
- return ProviderResponse(
176
+ self.logger.debug("Getting NowPayments payment status", extra={
177
+ 'payment_id': provider_payment_id
178
+ })
179
+
180
+ headers = {
181
+ 'x-api-key': self.config.api_key
182
+ }
183
+
184
+ response_data = self._make_request(
185
+ method='GET',
186
+ endpoint=f'payment/{provider_payment_id}',
187
+ headers=headers
188
+ )
189
+
190
+ if 'payment_status' in response_data:
191
+ return self._create_provider_response(
154
192
  success=True,
155
- provider_payment_id=response.get('payment_id'),
156
- status=universal_status,
157
- pay_address=response.get('pay_address'),
158
- amount=Decimal(str(response.get('pay_amount', 0))),
159
- currency=response.get('pay_currency')
193
+ raw_response=response_data,
194
+ provider_payment_id=provider_payment_id,
195
+ status=response_data['payment_status'],
196
+ amount=Decimal(str(response_data.get('pay_amount', 0))),
197
+ currency=response_data.get('pay_currency'),
198
+ wallet_address=response_data.get('pay_address')
160
199
  )
161
200
  else:
162
- return ProviderResponse(
201
+ error_message = response_data.get('message', 'Payment not found')
202
+ return self._create_provider_response(
163
203
  success=False,
164
- error_message='Payment not found'
204
+ raw_response=response_data,
205
+ error_message=error_message
165
206
  )
166
207
 
167
208
  except Exception as e:
168
- logger.error(f"NowPayments check_payment_status error: {e}")
169
- return ProviderResponse(
209
+ self.logger.error(f"NowPayments status check failed: {e}", extra={
210
+ 'payment_id': provider_payment_id
211
+ })
212
+
213
+ return self._create_provider_response(
170
214
  success=False,
171
- error_message=str(e)
215
+ raw_response={'error': str(e)},
216
+ error_message=f'Status check failed: {e}'
172
217
  )
173
218
 
174
- def process_webhook(self, payload: dict) -> WebhookData:
175
- """Process NowPayments webhook."""
219
+ def get_supported_currencies(self) -> ServiceOperationResult:
220
+ """
221
+ Get supported currencies from NowPayments.
222
+
223
+ Returns:
224
+ ServiceOperationResult: List of supported currencies
225
+ """
176
226
  try:
177
- # Map status
178
- status_mapping = {
179
- 'waiting': 'pending',
180
- 'confirming': 'processing',
181
- 'confirmed': 'completed',
182
- 'sending': 'processing',
183
- 'partially_paid': 'pending',
184
- 'finished': 'completed',
185
- 'failed': 'failed',
186
- 'refunded': 'refunded',
187
- 'expired': 'expired'
188
- }
227
+ self.logger.debug("Getting NowPayments supported currencies")
189
228
 
190
- provider_status = payload.get('payment_status', 'unknown')
191
- universal_status = status_mapping.get(provider_status, 'unknown')
229
+ headers = {
230
+ 'x-api-key': self.config.api_key
231
+ }
192
232
 
193
- return WebhookData(
194
- provider_payment_id=str(payload.get('payment_id', '')),
195
- status=universal_status,
196
- pay_amount=Decimal(str(payload.get('pay_amount', 0))),
197
- actually_paid=Decimal(str(payload.get('actually_paid', 0))),
198
- order_id=payload.get('order_id'),
199
- signature=payload.get('signature')
233
+ response_data = self._make_request(
234
+ method='GET',
235
+ endpoint='currencies',
236
+ headers=headers
200
237
  )
201
238
 
202
- except Exception as e:
203
- logger.error(f"NowPayments webhook processing error: {e}")
204
- raise
205
-
206
- def get_supported_currencies(self) -> List[str]:
207
- """Get list of supported currencies."""
208
- try:
209
- response = self._make_request('GET', 'currencies')
210
-
211
- if response and 'currencies' in response:
212
- return response['currencies']
239
+ if 'currencies' in response_data:
240
+ currencies = response_data['currencies']
241
+
242
+ return ServiceOperationResult(
243
+ success=True,
244
+ message=f"Retrieved {len(currencies)} supported currencies",
245
+ data={
246
+ 'currencies': currencies,
247
+ 'count': len(currencies),
248
+ 'provider': self.name
249
+ }
250
+ )
213
251
  else:
214
- # Fallback currencies
215
- return ['BTC', 'ETH', 'LTC', 'BCH', 'XMR', 'TRX', 'BNB']
252
+ return ServiceOperationResult(
253
+ success=False,
254
+ message="Failed to get currencies from NowPayments",
255
+ error_code="currencies_fetch_failed"
256
+ )
216
257
 
217
258
  except Exception as e:
218
- logger.error(f"Error getting supported currencies: {e}")
219
- return ['BTC', 'ETH', 'LTC'] # Minimal fallback
259
+ self.logger.error(f"NowPayments currencies fetch failed: {e}")
260
+
261
+ return ServiceOperationResult(
262
+ success=False,
263
+ message=f"Currencies fetch error: {e}",
264
+ error_code="currencies_fetch_error"
265
+ )
220
266
 
221
- def get_minimum_payment_amount(self, currency_from: str, currency_to: str = 'usd') -> Optional[Decimal]:
222
- """Get minimum payment amount for currency pair."""
267
+ def validate_webhook(self, payload: Dict[str, Any], signature: str = None) -> ServiceOperationResult:
268
+ """
269
+ Validate NowPayments IPN webhook.
270
+
271
+ Args:
272
+ payload: Webhook payload
273
+ signature: HMAC signature (optional)
274
+
275
+ Returns:
276
+ ServiceOperationResult: Validation result
277
+ """
223
278
  try:
224
- response = self._make_request('GET', 'min-amount', {
225
- 'currency_from': currency_from,
226
- 'currency_to': currency_to
279
+ self.logger.debug("Validating NowPayments webhook", extra={
280
+ 'has_signature': bool(signature),
281
+ 'payment_id': payload.get('payment_id')
227
282
  })
228
283
 
229
- if response and 'min_amount' in response:
230
- return Decimal(str(response['min_amount']))
284
+ # Validate payload structure
285
+ try:
286
+ webhook_data = NowPaymentsWebhook(**payload)
287
+ except Exception as e:
288
+ return ServiceOperationResult(
289
+ success=False,
290
+ message=f"Invalid webhook payload: {e}",
291
+ error_code="invalid_payload"
292
+ )
231
293
 
232
- return None
294
+ # Validate signature if provided and secret is configured
295
+ if signature and self.config.ipn_secret:
296
+ is_valid_signature = self._validate_ipn_signature(payload, signature)
297
+ if not is_valid_signature:
298
+ return ServiceOperationResult(
299
+ success=False,
300
+ message="Invalid webhook signature",
301
+ error_code="invalid_signature"
302
+ )
303
+
304
+ return ServiceOperationResult(
305
+ success=True,
306
+ message="Webhook validated successfully",
307
+ data={
308
+ 'provider': self.name,
309
+ 'payment_id': webhook_data.payment_id,
310
+ 'status': webhook_data.payment_status,
311
+ 'signature_validated': bool(signature and self.config.ipn_secret),
312
+ 'webhook_data': webhook_data.model_dump()
313
+ }
314
+ )
233
315
 
234
316
  except Exception as e:
235
- logger.error(f"Error getting minimum amount: {e}")
236
- return None
317
+ self.logger.error(f"NowPayments webhook validation failed: {e}")
318
+
319
+ return ServiceOperationResult(
320
+ success=False,
321
+ message=f"Webhook validation error: {e}",
322
+ error_code="validation_error"
323
+ )
237
324
 
238
- def estimate_payment_amount(self, amount: Decimal, currency_code: str) -> Optional[dict]:
239
- """Estimate payment amount in target currency."""
325
+ def get_exchange_rate(self, from_currency: str, to_currency: str) -> ServiceOperationResult:
326
+ """
327
+ Get exchange rate from NowPayments.
328
+
329
+ Args:
330
+ from_currency: Source currency
331
+ to_currency: Target currency
332
+
333
+ Returns:
334
+ ServiceOperationResult: Exchange rate
335
+ """
240
336
  try:
241
- response = self._make_request('GET', 'estimate', {
242
- 'amount': float(amount),
243
- 'currency_from': 'usd',
244
- 'currency_to': currency_code
337
+ self.logger.debug("Getting NowPayments exchange rate", extra={
338
+ 'from': from_currency,
339
+ 'to': to_currency
245
340
  })
246
341
 
247
- if response and 'estimated_amount' in response:
248
- return {
249
- 'estimated_amount': Decimal(str(response['estimated_amount'])),
250
- 'currency_from': response.get('currency_from'),
251
- 'currency_to': response.get('currency_to'),
252
- 'fee_amount': Decimal(str(response.get('fee_amount', 0)))
253
- }
342
+ headers = {
343
+ 'x-api-key': self.config.api_key
344
+ }
254
345
 
255
- return None
346
+ response_data = self._make_request(
347
+ method='GET',
348
+ endpoint=f'exchange-amount/{from_currency}-{to_currency}',
349
+ headers=headers
350
+ )
256
351
 
352
+ if 'estimated_amount' in response_data:
353
+ rate = float(response_data['estimated_amount'])
354
+
355
+ return ServiceOperationResult(
356
+ success=True,
357
+ message="Exchange rate retrieved",
358
+ data={
359
+ 'from_currency': from_currency,
360
+ 'to_currency': to_currency,
361
+ 'rate': rate,
362
+ 'provider': self.name
363
+ }
364
+ )
365
+ else:
366
+ return ServiceOperationResult(
367
+ success=False,
368
+ message="Exchange rate not available",
369
+ error_code="rate_not_available"
370
+ )
371
+
257
372
  except Exception as e:
258
- logger.error(f"Error estimating payment amount: {e}")
259
- return None
373
+ self.logger.error(f"NowPayments exchange rate failed: {e}")
374
+
375
+ return ServiceOperationResult(
376
+ success=False,
377
+ message=f"Exchange rate error: {e}",
378
+ error_code="rate_fetch_error"
379
+ )
260
380
 
261
- def validate_webhook(self, payload: dict, headers: Optional[dict] = None) -> bool:
262
- """Validate NowPayments webhook signature."""
263
- try:
264
- if not self.ipn_secret:
265
- logger.warning("IPN secret not configured, skipping webhook validation")
266
- return True
381
+ def _validate_ipn_signature(self, payload: Dict[str, Any], signature: str) -> bool:
382
+ """
383
+ Validate IPN signature using HMAC-SHA512.
384
+
385
+ Args:
386
+ payload: Webhook payload
387
+ signature: Received signature
267
388
 
268
- if not headers:
269
- logger.warning("No headers provided for webhook validation")
270
- return False
389
+ Returns:
390
+ bool: True if signature is valid
391
+ """
392
+ try:
393
+ # Sort payload and create canonical string
394
+ sorted_payload = json.dumps(payload, separators=(',', ':'), sort_keys=True)
271
395
 
272
- # Get signature from headers
273
- signature = headers.get('x-nowpayments-sig')
274
- if not signature:
275
- logger.warning("No signature found in webhook headers")
276
- return False
396
+ # Calculate expected signature
397
+ expected_signature = hmac.new(
398
+ self.config.ipn_secret.encode('utf-8'),
399
+ sorted_payload.encode('utf-8'),
400
+ hashlib.sha512
401
+ ).hexdigest()
277
402
 
278
- # TODO: Implement proper HMAC signature validation
279
- # This requires the raw payload body for proper validation
280
- logger.info("Webhook signature validation placeholder")
281
- return True
403
+ # Compare signatures
404
+ return hmac.compare_digest(expected_signature, signature)
282
405
 
283
406
  except Exception as e:
284
- logger.error(f"Webhook validation error: {e}")
407
+ self.logger.error(f"Signature validation error: {e}")
285
408
  return False
286
409
 
287
- def check_api_status(self) -> bool:
288
- """Check if NowPayments API is available."""
410
+ def _parse_expiry_time(self, expiry_str: Optional[str]) -> Optional[datetime]:
411
+ """
412
+ Parse NowPayments expiry time string.
413
+
414
+ Args:
415
+ expiry_str: Expiry time string from NowPayments
416
+
417
+ Returns:
418
+ Optional[datetime]: Parsed expiry time
419
+ """
420
+ if not expiry_str:
421
+ return None
422
+
289
423
  try:
290
- response = self._make_request('GET', 'status')
291
- return response is not None and response.get('message') == 'OK'
292
- except:
293
- return False
424
+ # NowPayments typically returns ISO format
425
+ return datetime.fromisoformat(expiry_str.replace('Z', '+00:00'))
426
+ except Exception:
427
+ self.logger.warning(f"Failed to parse expiry time: {expiry_str}")
428
+ return None
429
+
430
+ def set_ipn_callback_url(self, callback_url: str):
431
+ """
432
+ Set IPN callback URL for payments.
433
+
434
+ Args:
435
+ callback_url: IPN callback URL
436
+ """
437
+ self._ipn_callback_url = callback_url
438
+ self.logger.info(f"Set IPN callback URL: {callback_url}")
439
+
440
+ def health_check(self) -> ServiceOperationResult:
441
+ """Perform NowPayments-specific health check."""
442
+ try:
443
+ # Test API connectivity by getting currencies
444
+ currencies_result = self.get_supported_currencies()
445
+
446
+ if currencies_result.success:
447
+ currency_count = len(currencies_result.data.get('currencies', []))
448
+
449
+ return ServiceOperationResult(
450
+ success=True,
451
+ message="NowPayments provider is healthy",
452
+ data={
453
+ 'provider': self.name,
454
+ 'sandbox': self.is_sandbox,
455
+ 'api_url': self.config.api_url,
456
+ 'supported_currencies': currency_count,
457
+ 'has_ipn_secret': bool(self.config.ipn_secret),
458
+ 'api_key_configured': bool(self.config.api_key)
459
+ }
460
+ )
461
+ else:
462
+ return ServiceOperationResult(
463
+ success=False,
464
+ message="NowPayments API connectivity failed",
465
+ error_code="api_connectivity_failed",
466
+ data={
467
+ 'provider': self.name,
468
+ 'error': currencies_result.message
469
+ }
470
+ )
471
+
472
+ except Exception as e:
473
+ return ServiceOperationResult(
474
+ success=False,
475
+ message=f"NowPayments health check error: {e}",
476
+ error_code="health_check_error",
477
+ data={'provider': self.name}
478
+ )