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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -9
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +600 -108
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +470 -64
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
- django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +16 -20
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +207 -67
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -284
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +344 -468
- django_cfg/apps/payments/services/core/subscription_service.py +425 -484
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +232 -71
- django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
- django_cfg/apps/payments/services/providers/registry.py +429 -80
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +211 -130
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +129 -98
- django_cfg/apps/payments/signals/subscription_signals.py +195 -143
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +46 -47
- django_cfg/apps/payments/urls_admin.py +49 -0
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +75 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -498
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +65 -12
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -178
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/managers/__init__.py +0 -22
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -83
- django_cfg/apps/payments/managers/payment_manager.py +0 -44
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -56
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -55
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -297
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -637
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/urls_templates.py +0 -52
- django_cfg/apps/payments/utils/__init__.py +0 -45
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -245
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -62
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -111
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -312
- django_cfg/apps/payments/views/templates/base.py +0 -204
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -164
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -240
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -65
- django_cfg/core/integration.py +0 -160
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {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
|
2
|
+
NowPayments provider for the Universal Payment System v2.0.
|
3
3
|
|
4
|
-
|
4
|
+
Implementation of NowPayments API integration with unified interface.
|
5
5
|
"""
|
6
6
|
|
7
|
-
import
|
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
|
16
|
-
from ..
|
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(
|
22
|
-
"""
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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(
|
33
|
-
"""
|
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
|
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
|
55
|
-
"""
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
74
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
99
|
-
if
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
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
|
-
|
109
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
amount=Decimal(str(
|
115
|
-
currency=
|
116
|
-
|
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
|
-
|
146
|
+
# Error response
|
147
|
+
error_message = response_data.get('message', 'Unknown error')
|
148
|
+
return self._create_provider_response(
|
120
149
|
success=False,
|
121
|
-
|
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
|
126
|
-
|
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
|
-
|
161
|
+
raw_response={'error': str(e)},
|
162
|
+
error_message=f'Payment creation failed: {e}'
|
129
163
|
)
|
130
164
|
|
131
|
-
def
|
132
|
-
"""
|
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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
158
|
-
amount=Decimal(str(
|
159
|
-
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
|
-
|
201
|
+
error_message = response_data.get('message', 'Payment not found')
|
202
|
+
return self._create_provider_response(
|
163
203
|
success=False,
|
164
|
-
|
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
|
169
|
-
|
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
|
-
|
215
|
+
raw_response={'error': str(e)},
|
216
|
+
error_message=f'Status check failed: {e}'
|
172
217
|
)
|
173
218
|
|
174
|
-
def
|
175
|
-
"""
|
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
|
-
|
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
|
-
|
191
|
-
|
229
|
+
headers = {
|
230
|
+
'x-api-key': self.config.api_key
|
231
|
+
}
|
192
232
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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
|
-
|
215
|
-
|
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"
|
219
|
-
|
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
|
222
|
-
"""
|
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
|
-
|
225
|
-
'
|
226
|
-
'
|
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
|
-
|
230
|
-
|
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
|
-
|
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"
|
236
|
-
|
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
|
239
|
-
"""
|
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
|
-
|
242
|
-
'
|
243
|
-
'
|
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
|
-
|
248
|
-
|
249
|
-
|
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
|
-
|
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"
|
259
|
-
|
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
|
262
|
-
"""
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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
|
-
|
269
|
-
|
270
|
-
|
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
|
-
#
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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
|
-
#
|
279
|
-
|
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"
|
407
|
+
self.logger.error(f"Signature validation error: {e}")
|
285
408
|
return False
|
286
409
|
|
287
|
-
def
|
288
|
-
"""
|
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
|
-
|
291
|
-
return
|
292
|
-
except:
|
293
|
-
|
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
|
+
)
|