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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/admin/__init__.py +24 -8
- django_cfg/apps/accounts/admin/activity_admin.py +146 -0
- django_cfg/apps/accounts/admin/filters.py +98 -22
- django_cfg/apps/accounts/admin/group_admin.py +86 -0
- django_cfg/apps/accounts/admin/inlines.py +42 -13
- django_cfg/apps/accounts/admin/otp_admin.py +115 -0
- django_cfg/apps/accounts/admin/registration_admin.py +173 -0
- django_cfg/apps/accounts/admin/resources.py +123 -19
- django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
- django_cfg/apps/accounts/admin/user_admin.py +362 -0
- django_cfg/apps/agents/admin/__init__.py +17 -4
- django_cfg/apps/agents/admin/execution_admin.py +204 -183
- django_cfg/apps/agents/admin/registry_admin.py +230 -255
- django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
- django_cfg/apps/agents/core/__init__.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +221 -0
- django_cfg/apps/agents/core/exceptions.py +14 -0
- django_cfg/apps/agents/core/orchestrator.py +18 -3
- django_cfg/apps/knowbase/admin/__init__.py +1 -1
- django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
- django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
- django_cfg/apps/knowbase/admin/document_admin.py +269 -262
- django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
- django_cfg/apps/knowbase/config/settings.py +21 -4
- django_cfg/apps/knowbase/views/chat_views.py +3 -0
- django_cfg/apps/leads/admin/__init__.py +3 -1
- django_cfg/apps/leads/admin/leads_admin.py +235 -35
- django_cfg/apps/maintenance/admin/__init__.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
- django_cfg/apps/maintenance/admin/log_admin.py +143 -61
- django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
- django_cfg/apps/maintenance/admin/site_admin.py +213 -352
- django_cfg/apps/newsletter/admin/__init__.py +29 -2
- django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
- django_cfg/apps/payments/admin/__init__.py +18 -27
- django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
- django_cfg/apps/payments/admin/balance_admin.py +166 -632
- django_cfg/apps/payments/admin/currencies_admin.py +235 -607
- django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
- django_cfg/apps/payments/admin/filters.py +83 -3
- django_cfg/apps/payments/admin/networks_admin.py +269 -0
- django_cfg/apps/payments/admin/payments_admin.py +183 -460
- django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
- django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +153 -34
- django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +43 -17
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +109 -63
- django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
- django_cfg/apps/payments/config/__init__.py +14 -15
- django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
- django_cfg/apps/payments/config/helpers.py +8 -13
- django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
- django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
- django_cfg/apps/payments/middleware/api_access.py +32 -6
- django_cfg/apps/payments/migrations/0001_initial.py +33 -46
- django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
- django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
- django_cfg/apps/payments/models/balance.py +12 -0
- django_cfg/apps/payments/models/currencies.py +106 -32
- django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
- django_cfg/apps/payments/models/payments.py +94 -0
- django_cfg/apps/payments/services/core/base.py +4 -4
- django_cfg/apps/payments/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +266 -39
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +303 -41
- django_cfg/apps/payments/services/providers/models/__init__.py +42 -0
- django_cfg/apps/payments/services/providers/models/base.py +145 -0
- django_cfg/apps/payments/services/providers/models/providers.py +87 -0
- django_cfg/apps/payments/services/providers/models/universal.py +48 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
- django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +557 -0
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +9 -37
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/services/types/requests.py +19 -7
- django_cfg/apps/payments/signals/payment_signals.py +31 -2
- django_cfg/apps/payments/static/payments/js/api-client.js +29 -6
- django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
- django_cfg/apps/payments/static/payments/js/payment-form.js +98 -32
- django_cfg/apps/payments/tasks/__init__.py +39 -0
- django_cfg/apps/payments/tasks/types.py +73 -0
- django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
- django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
- django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
- django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
- django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
- django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
- django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
- django_cfg/apps/payments/urls.py +3 -2
- django_cfg/apps/payments/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +8 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +22 -8
- django_cfg/apps/support/admin/__init__.py +10 -1
- django_cfg/apps/support/admin/support_admin.py +338 -141
- django_cfg/apps/tasks/admin/__init__.py +11 -0
- django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
- django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
- django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
- django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
- django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
- django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
- django_cfg/apps/tasks/tasks/__init__.py +10 -0
- django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
- django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
- django_cfg/apps/tasks/templates/tasks/components/{status_cards.html → overview_content.html} +30 -11
- django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
- django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
- django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
- django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
- django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
- django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
- django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
- django_cfg/apps/tasks/urls.py +2 -2
- django_cfg/apps/tasks/urls_admin.py +2 -2
- django_cfg/apps/tasks/utils/__init__.py +1 -0
- django_cfg/apps/tasks/utils/simulator.py +356 -0
- django_cfg/apps/tasks/views/__init__.py +16 -0
- django_cfg/apps/tasks/views/api.py +569 -0
- django_cfg/apps/tasks/views/dashboard.py +58 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/core/integration/__init__.py +21 -0
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/migrate_all.py +9 -3
- django_cfg/management/commands/migrator.py +11 -6
- django_cfg/management/commands/rundramatiq.py +3 -2
- django_cfg/management/commands/rundramatiq_simulator.py +430 -0
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- django_cfg/models/constance.py +0 -11
- django_cfg/models/payments.py +137 -3
- django_cfg/modules/django_admin/__init__.py +64 -0
- django_cfg/modules/django_admin/decorators/__init__.py +13 -0
- django_cfg/modules/django_admin/decorators/actions.py +106 -0
- django_cfg/modules/django_admin/decorators/display.py +106 -0
- django_cfg/modules/django_admin/mixins/__init__.py +14 -0
- django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
- django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
- django_cfg/modules/django_admin/models/__init__.py +20 -0
- django_cfg/modules/django_admin/models/action_models.py +33 -0
- django_cfg/modules/django_admin/models/badge_models.py +20 -0
- django_cfg/modules/django_admin/models/base.py +26 -0
- django_cfg/modules/django_admin/models/display_models.py +31 -0
- django_cfg/modules/django_admin/utils/badges.py +159 -0
- django_cfg/modules/django_admin/utils/displays.py +247 -0
- django_cfg/modules/django_currency/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
- django_cfg/modules/django_currency/core/converter.py +12 -12
- django_cfg/modules/django_currency/database/__init__.py +2 -2
- django_cfg/modules/django_currency/database/database_loader.py +93 -42
- django_cfg/modules/django_llm/llm/client.py +10 -2
- django_cfg/modules/django_tasks.py +54 -21
- django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
- django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
- django_cfg/modules/django_unfold/dashboard.py +14 -13
- django_cfg/modules/django_unfold/models/config.py +1 -1
- django_cfg/registry/core.py +7 -9
- django_cfg/registry/third_party.py +2 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -1
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/RECORD +198 -160
- django_cfg/apps/accounts/admin/activity.py +0 -96
- django_cfg/apps/accounts/admin/group.py +0 -17
- django_cfg/apps/accounts/admin/otp.py +0 -59
- django_cfg/apps/accounts/admin/registration_source.py +0 -97
- django_cfg/apps/accounts/admin/twilio_response.py +0 -227
- django_cfg/apps/accounts/admin/user.py +0 -300
- django_cfg/apps/agents/core/agent.py +0 -281
- django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
- django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
- django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
- django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
- django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
- django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
- django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
- django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
- django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
- django_cfg/apps/payments/config/constance/__init__.py +0 -22
- django_cfg/apps/payments/config/constance/config_service.py +0 -123
- django_cfg/apps/payments/config/constance/fields.py +0 -69
- django_cfg/apps/payments/config/constance/settings.py +0 -160
- django_cfg/apps/payments/services/providers/nowpayments.py +0 -478
- django_cfg/apps/tasks/admin.py +0 -320
- django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
- django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
- django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
- django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
- django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
- django_cfg/apps/tasks/templates/tasks/base.html +0 -96
- django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
- django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
- django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
- django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
- django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
- django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
- django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
- django_cfg/apps/tasks/views.py +0 -461
- django_cfg/management/commands/auto_generate.py +0 -486
- django_cfg/middleware/static_nocache.py +0 -55
- django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
- /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
- {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)
|