django-cfg 1.3.9__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/payments/admin/networks_admin.py +12 -1
- django_cfg/apps/payments/admin/payments_admin.py +13 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +62 -14
- 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 +33 -3
- django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +96 -45
- 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/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/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/payment_service.py +265 -38
- django_cfg/apps/payments/services/providers/base.py +209 -3
- django_cfg/apps/payments/services/providers/models/__init__.py +2 -0
- django_cfg/apps/payments/services/providers/models/base.py +25 -2
- django_cfg/apps/payments/services/providers/nowpayments/models.py +2 -2
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +57 -9
- django_cfg/apps/payments/services/providers/registry.py +5 -5
- 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 +6 -1
- django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
- django_cfg/apps/payments/static/payments/js/payment-form.js +35 -26
- django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
- django_cfg/apps/payments/urls.py +3 -2
- django_cfg/apps/payments/views/api/currencies.py +3 -0
- django_cfg/apps/payments/views/serializers/currencies.py +18 -5
- django_cfg/apps/tasks/admin/tasks_admin.py +2 -2
- 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/core/integration/__init__.py +21 -0
- django_cfg/management/commands/rundramatiq_simulator.py +430 -0
- django_cfg/models/constance.py +0 -11
- django_cfg/models/payments.py +137 -3
- django_cfg/modules/django_tasks.py +54 -21
- django_cfg/registry/core.py +4 -9
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -2
- {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/RECORD +84 -152
- 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/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +0 -26
- django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +0 -28
- django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +0 -30
- 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/app_agent_diagnose.py +0 -470
- django_cfg/management/commands/app_agent_generate.py +0 -342
- django_cfg/management/commands/app_agent_info.py +0 -308
- django_cfg/management/commands/auto_generate.py +0 -486
- django_cfg/modules/django_app_agent/__init__.py +0 -87
- django_cfg/modules/django_app_agent/agents/__init__.py +0 -40
- django_cfg/modules/django_app_agent/agents/base/__init__.py +0 -24
- django_cfg/modules/django_app_agent/agents/base/agent.py +0 -354
- django_cfg/modules/django_app_agent/agents/base/context.py +0 -236
- django_cfg/modules/django_app_agent/agents/base/executor.py +0 -430
- django_cfg/modules/django_app_agent/agents/generation/__init__.py +0 -12
- django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +0 -15
- django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +0 -147
- django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +0 -99
- django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +0 -32
- django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +0 -290
- django_cfg/modules/django_app_agent/agents/interfaces.py +0 -376
- django_cfg/modules/django_app_agent/core/__init__.py +0 -33
- django_cfg/modules/django_app_agent/core/config.py +0 -300
- django_cfg/modules/django_app_agent/core/exceptions.py +0 -359
- django_cfg/modules/django_app_agent/models/__init__.py +0 -71
- django_cfg/modules/django_app_agent/models/base.py +0 -283
- django_cfg/modules/django_app_agent/models/context.py +0 -496
- django_cfg/modules/django_app_agent/models/enums.py +0 -481
- django_cfg/modules/django_app_agent/models/requests.py +0 -500
- django_cfg/modules/django_app_agent/models/responses.py +0 -585
- django_cfg/modules/django_app_agent/pytest.ini +0 -6
- django_cfg/modules/django_app_agent/services/__init__.py +0 -42
- django_cfg/modules/django_app_agent/services/app_generator/__init__.py +0 -30
- django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +0 -133
- django_cfg/modules/django_app_agent/services/app_generator/context.py +0 -40
- django_cfg/modules/django_app_agent/services/app_generator/main.py +0 -202
- django_cfg/modules/django_app_agent/services/app_generator/structure.py +0 -316
- django_cfg/modules/django_app_agent/services/app_generator/validation.py +0 -125
- django_cfg/modules/django_app_agent/services/base.py +0 -437
- django_cfg/modules/django_app_agent/services/context_builder/__init__.py +0 -34
- django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +0 -141
- django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +0 -276
- django_cfg/modules/django_app_agent/services/context_builder/main.py +0 -272
- django_cfg/modules/django_app_agent/services/context_builder/models.py +0 -40
- django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +0 -85
- django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +0 -31
- django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +0 -311
- django_cfg/modules/django_app_agent/services/project_scanner/main.py +0 -221
- django_cfg/modules/django_app_agent/services/project_scanner/models.py +0 -59
- django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +0 -94
- django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +0 -28
- django_cfg/modules/django_app_agent/services/questioning_service/main.py +0 -273
- django_cfg/modules/django_app_agent/services/questioning_service/models.py +0 -111
- django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +0 -251
- django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +0 -347
- django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +0 -356
- django_cfg/modules/django_app_agent/services/report_service.py +0 -332
- django_cfg/modules/django_app_agent/services/template_manager/__init__.py +0 -18
- django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +0 -236
- django_cfg/modules/django_app_agent/services/template_manager/main.py +0 -159
- django_cfg/modules/django_app_agent/services/template_manager/models.py +0 -36
- django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +0 -100
- django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +0 -105
- django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +0 -31
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +0 -44
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +0 -81
- django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +0 -107
- django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +0 -139
- django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +0 -91
- django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +0 -195
- django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +0 -35
- django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +0 -211
- django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +0 -200
- django_cfg/modules/django_app_agent/services/validation_service/__init__.py +0 -25
- django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +0 -333
- django_cfg/modules/django_app_agent/services/validation_service/main.py +0 -242
- django_cfg/modules/django_app_agent/services/validation_service/models.py +0 -66
- django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +0 -352
- django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +0 -272
- django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +0 -203
- django_cfg/modules/django_app_agent/ui/__init__.py +0 -25
- django_cfg/modules/django_app_agent/ui/cli.py +0 -419
- django_cfg/modules/django_app_agent/ui/rich_components.py +0 -622
- django_cfg/modules/django_app_agent/utils/__init__.py +0 -38
- django_cfg/modules/django_app_agent/utils/logging.py +0 -360
- django_cfg/modules/django_app_agent/utils/validation.py +0 -417
- {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -27,9 +27,9 @@ class NowPaymentsProviderConfig(ProviderConfig):
|
|
27
27
|
|
28
28
|
if 'api_url' not in data:
|
29
29
|
# TEMP: Force production URL since sandbox is down
|
30
|
-
#
|
30
|
+
# sandbox_mode = data.get('sandbox_mode', False)
|
31
31
|
# data['api_url'] = (
|
32
|
-
# 'https://api-sandbox.nowpayments.io/v1' if
|
32
|
+
# 'https://api-sandbox.nowpayments.io/v1' if sandbox_mode
|
33
33
|
# else 'https://api.nowpayments.io/v1'
|
34
34
|
# )
|
35
35
|
data['api_url'] = 'https://api.nowpayments.io/v1' # Force production
|
@@ -99,15 +99,27 @@ class NowPaymentsProvider(BaseProvider):
|
|
99
99
|
'order_id': request.order_id
|
100
100
|
})
|
101
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
|
+
|
102
105
|
# Prepare NowPayments request
|
103
106
|
payment_data = {
|
104
107
|
'price_amount': request.amount_usd,
|
105
108
|
'price_currency': 'USD',
|
106
|
-
'pay_currency':
|
109
|
+
'pay_currency': provider_currency_code, # Use provider-specific currency code
|
107
110
|
'order_id': request.order_id,
|
108
111
|
'order_description': request.description or f'Payment {request.order_id}',
|
109
112
|
}
|
110
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
|
+
|
111
123
|
# Add optional fields
|
112
124
|
if request.callback_url:
|
113
125
|
payment_data['success_url'] = request.callback_url
|
@@ -136,6 +148,14 @@ class NowPaymentsProvider(BaseProvider):
|
|
136
148
|
|
137
149
|
# Parse NowPayments response
|
138
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
|
+
|
139
159
|
# Successful payment creation
|
140
160
|
payment_url = response_data.get('invoice_url') or response_data.get('pay_url')
|
141
161
|
|
@@ -160,14 +180,30 @@ class NowPaymentsProvider(BaseProvider):
|
|
160
180
|
)
|
161
181
|
|
162
182
|
except Exception as e:
|
163
|
-
|
164
|
-
|
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__
|
165
187
|
})
|
166
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
|
+
|
167
203
|
return self._create_provider_response(
|
168
204
|
success=False,
|
169
|
-
raw_response={'error':
|
170
|
-
error_message=
|
205
|
+
raw_response={'error': error_msg, 'error_type': type(e).__name__},
|
206
|
+
error_message=user_message
|
171
207
|
)
|
172
208
|
|
173
209
|
def get_payment_status(self, provider_payment_id: str) -> ProviderResponse:
|
@@ -209,14 +245,26 @@ class NowPaymentsProvider(BaseProvider):
|
|
209
245
|
)
|
210
246
|
|
211
247
|
except Exception as e:
|
212
|
-
|
213
|
-
|
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__
|
214
252
|
})
|
215
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
|
+
|
216
264
|
return self._create_provider_response(
|
217
265
|
success=False,
|
218
|
-
raw_response={'error':
|
219
|
-
error_message=
|
266
|
+
raw_response={'error': error_msg, 'error_type': type(e).__name__},
|
267
|
+
error_message=user_message
|
220
268
|
)
|
221
269
|
|
222
270
|
def get_supported_currencies(self) -> ServiceOperationResult:
|
@@ -24,10 +24,10 @@ class ProviderRegistry:
|
|
24
24
|
|
25
25
|
def __init__(self):
|
26
26
|
"""Initialize provider registry."""
|
27
|
-
# Use
|
28
|
-
from ...config.
|
27
|
+
# Use PaymentsConfigManager for configuration from BaseCfgAutoModule
|
28
|
+
from ...config.django_cfg_integration import PaymentsConfigManager
|
29
29
|
|
30
|
-
self.
|
30
|
+
self.config_manager = PaymentsConfigManager
|
31
31
|
self._providers: Dict[str, BaseProvider] = {}
|
32
32
|
|
33
33
|
self._provider_classes: Dict[str, Type[BaseProvider]] = {
|
@@ -50,9 +50,9 @@ class ProviderRegistry:
|
|
50
50
|
try:
|
51
51
|
logger.info("Initializing provider registry")
|
52
52
|
|
53
|
-
# Get all provider configurations
|
53
|
+
# Get all provider configurations from BaseCfgAutoModule
|
54
54
|
try:
|
55
|
-
provider_configs = self.
|
55
|
+
provider_configs = self.config_manager.get_all_provider_configs()
|
56
56
|
except Exception as e:
|
57
57
|
return ServiceOperationResult(
|
58
58
|
success=False,
|
@@ -24,8 +24,8 @@ class PaymentCreateRequest(BaseModel):
|
|
24
24
|
|
25
25
|
user_id: int = Field(gt=0, description="User ID")
|
26
26
|
amount_usd: float = Field(gt=1.0, le=50000.0, description="Amount in USD")
|
27
|
-
currency_code:
|
28
|
-
description="Cryptocurrency code"
|
27
|
+
currency_code: str = Field(
|
28
|
+
min_length=2, max_length=10, description="Cryptocurrency code"
|
29
29
|
)
|
30
30
|
provider: Literal['nowpayments'] = Field(default='nowpayments', description="Payment provider")
|
31
31
|
callback_url: Optional[str] = Field(None, description="Success callback URL")
|
@@ -36,11 +36,23 @@ class PaymentCreateRequest(BaseModel):
|
|
36
36
|
@field_validator('currency_code')
|
37
37
|
@classmethod
|
38
38
|
def validate_currency(cls, v: str) -> str:
|
39
|
-
"""Validate currency is supported."""
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
"""Validate currency is supported by checking database."""
|
40
|
+
from django_cfg.apps.payments.models import Currency
|
41
|
+
|
42
|
+
currency_code = v.upper().strip()
|
43
|
+
|
44
|
+
# Check if currency exists in database and is active
|
45
|
+
try:
|
46
|
+
currency = Currency.objects.get(code=currency_code, is_active=True)
|
47
|
+
return currency_code
|
48
|
+
except Currency.DoesNotExist:
|
49
|
+
# Get list of active currencies for error message
|
50
|
+
active_currencies = Currency.objects.filter(is_active=True).values_list('code', flat=True)[:10]
|
51
|
+
currency_list = ', '.join(active_currencies) + ('...' if len(active_currencies) == 10 else '')
|
52
|
+
raise ValueError(f"Currency {currency_code} not found or inactive. Available: {currency_list}")
|
53
|
+
except Exception:
|
54
|
+
# Fallback validation if database is not available
|
55
|
+
return currency_code
|
44
56
|
|
45
57
|
@field_validator('callback_url', 'cancel_url')
|
46
58
|
@classmethod
|
@@ -17,16 +17,45 @@ logger = get_logger("payment_signals")
|
|
17
17
|
|
18
18
|
|
19
19
|
@receiver(pre_save, sender=UniversalPayment)
|
20
|
-
def
|
21
|
-
"""
|
20
|
+
def handle_status_changes(sender, instance: UniversalPayment, **kwargs):
|
21
|
+
"""
|
22
|
+
Handle status changes and update status_changed_at field.
|
23
|
+
|
24
|
+
This signal automatically updates status_changed_at when status changes,
|
25
|
+
ensuring consistent tracking across all update methods.
|
26
|
+
"""
|
22
27
|
if instance.pk:
|
23
28
|
try:
|
24
29
|
original = UniversalPayment.objects.get(pk=instance.pk)
|
25
30
|
instance._original_status = original.status
|
31
|
+
|
32
|
+
# Check if status has changed
|
33
|
+
if original.status != instance.status:
|
34
|
+
instance.status_changed_at = timezone.now()
|
35
|
+
|
36
|
+
# Set completed_at if status changed to completed
|
37
|
+
if instance.status == 'completed' and not instance.completed_at:
|
38
|
+
instance.completed_at = timezone.now()
|
39
|
+
|
40
|
+
logger.debug(f"Status change detected in pre_save", extra={
|
41
|
+
'payment_id': str(instance.id),
|
42
|
+
'old_status': original.status,
|
43
|
+
'new_status': instance.status,
|
44
|
+
'status_changed_at': instance.status_changed_at.isoformat()
|
45
|
+
})
|
26
46
|
except UniversalPayment.DoesNotExist:
|
27
47
|
instance._original_status = None
|
28
48
|
else:
|
49
|
+
# New object - set status_changed_at if status is set
|
29
50
|
instance._original_status = None
|
51
|
+
if instance.status and not instance.status_changed_at:
|
52
|
+
instance.status_changed_at = timezone.now()
|
53
|
+
|
54
|
+
logger.debug(f"New payment status set in pre_save", extra={
|
55
|
+
'payment_id': 'new',
|
56
|
+
'status': instance.status,
|
57
|
+
'status_changed_at': instance.status_changed_at.isoformat()
|
58
|
+
})
|
30
59
|
|
31
60
|
|
32
61
|
@receiver(post_save, sender=UniversalPayment)
|
@@ -149,7 +149,11 @@ class PaymentAPIClient {
|
|
149
149
|
rates: (params = {}) => this.get(`${this.baseURL}/currencies/rates/`, params),
|
150
150
|
|
151
151
|
// Convert currency
|
152
|
-
convert: (from, to, amount) => this.post(`${this.baseURL}/currencies/convert/`, {
|
152
|
+
convert: (from, to, amount) => this.post(`${this.baseURL}/currencies/convert/`, {
|
153
|
+
from_currency: from,
|
154
|
+
to_currency: to,
|
155
|
+
amount: amount
|
156
|
+
}),
|
153
157
|
|
154
158
|
// Get provider-specific currency configurations
|
155
159
|
providerConfigs: (provider) => this.get(`${this.baseURL}/provider-currencies/`, { provider, page_size: 1000 }),
|
@@ -297,6 +301,7 @@ class PaymentAPIClient {
|
|
297
301
|
delete: (id) => this.delete(`${this.adminURL}/api/payments/${id}/`),
|
298
302
|
cancel: (id) => this.post(`${this.adminURL}/api/payments/${id}/cancel/`),
|
299
303
|
refund: (id) => this.post(`${this.adminURL}/api/payments/${id}/refund/`),
|
304
|
+
refreshStatus: (id) => this.post(`${this.adminURL}/api/payments/${id}/refresh_status/`),
|
300
305
|
stats: () => this.get(`${this.adminURL}/api/payments/stats/`)
|
301
306
|
},
|
302
307
|
|
@@ -0,0 +1,167 @@
|
|
1
|
+
/**
|
2
|
+
* Payment Detail Page JavaScript
|
3
|
+
* Handles payment detail page functionality including AJAX status refresh
|
4
|
+
*/
|
5
|
+
|
6
|
+
function paymentDetail() {
|
7
|
+
return {
|
8
|
+
loading: false,
|
9
|
+
showQRCode: false,
|
10
|
+
paymentId: null,
|
11
|
+
|
12
|
+
init(paymentId) {
|
13
|
+
this.paymentId = paymentId;
|
14
|
+
// Show QR code by default if payment address exists
|
15
|
+
const payAddress = document.querySelector('[data-field="pay_address"]');
|
16
|
+
if (payAddress && payAddress.textContent.trim()) {
|
17
|
+
this.showQRCode = true;
|
18
|
+
}
|
19
|
+
},
|
20
|
+
|
21
|
+
async refreshPaymentStatus() {
|
22
|
+
this.loading = true;
|
23
|
+
try {
|
24
|
+
// Use the existing PaymentAPI client
|
25
|
+
const data = await PaymentAPI.admin.payments.refreshStatus(this.paymentId);
|
26
|
+
|
27
|
+
if (data.success) {
|
28
|
+
// Show success notification
|
29
|
+
PaymentAPI.utils.showNotification(data.message, 'success');
|
30
|
+
|
31
|
+
// Update payment data on page without full reload
|
32
|
+
this.updatePaymentData(data.payment);
|
33
|
+
} else {
|
34
|
+
// Show error notification
|
35
|
+
PaymentAPI.utils.showNotification(data.message || 'Failed to refresh payment status', 'error');
|
36
|
+
}
|
37
|
+
} catch (error) {
|
38
|
+
console.error('Failed to refresh payment status:', error);
|
39
|
+
PaymentAPI.utils.showNotification('Network error while refreshing status', 'error');
|
40
|
+
} finally {
|
41
|
+
this.loading = false;
|
42
|
+
}
|
43
|
+
},
|
44
|
+
|
45
|
+
updatePaymentData(paymentData) {
|
46
|
+
// Update status badge
|
47
|
+
const statusBadge = document.querySelector('.payment-status-badge');
|
48
|
+
if (statusBadge && paymentData.status) {
|
49
|
+
statusBadge.textContent = paymentData.status.toUpperCase();
|
50
|
+
statusBadge.className = `payment-status-badge inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${this.getStatusBadgeClass(paymentData.status)}`;
|
51
|
+
}
|
52
|
+
|
53
|
+
// Update other fields that might have changed
|
54
|
+
const fields = {
|
55
|
+
'pay_amount': paymentData.pay_amount,
|
56
|
+
'transaction_hash': paymentData.transaction_hash,
|
57
|
+
'pay_address': paymentData.pay_address,
|
58
|
+
'payment_url': paymentData.payment_url,
|
59
|
+
'expires_at': paymentData.expires_at,
|
60
|
+
'confirmed_at': paymentData.confirmed_at,
|
61
|
+
'updated_at': paymentData.updated_at,
|
62
|
+
'status_changed_at': paymentData.status_changed_at
|
63
|
+
};
|
64
|
+
|
65
|
+
Object.entries(fields).forEach(([field, value]) => {
|
66
|
+
const element = document.querySelector(`[data-field="${field}"]`);
|
67
|
+
if (element && value !== null && value !== undefined) {
|
68
|
+
if (field.includes('_at') && value) {
|
69
|
+
// Format datetime fields
|
70
|
+
element.textContent = new Date(value).toLocaleString();
|
71
|
+
} else if (field === 'pay_amount' && paymentData.currency) {
|
72
|
+
// Format pay_amount with currency
|
73
|
+
element.textContent = `${parseFloat(value).toFixed(8)} ${paymentData.currency.code}`;
|
74
|
+
} else {
|
75
|
+
element.textContent = value;
|
76
|
+
}
|
77
|
+
}
|
78
|
+
});
|
79
|
+
|
80
|
+
// Update error info if present
|
81
|
+
if (paymentData.error_info) {
|
82
|
+
const errorElement = document.querySelector('[data-field="error_info"]');
|
83
|
+
if (errorElement) {
|
84
|
+
errorElement.textContent = JSON.stringify(paymentData.error_info, null, 2);
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
// Update QR code visibility if pay_address changed
|
89
|
+
if (paymentData.pay_address && !this.showQRCode) {
|
90
|
+
this.showQRCode = true;
|
91
|
+
}
|
92
|
+
},
|
93
|
+
|
94
|
+
getStatusBadgeClass(status) {
|
95
|
+
const statusClasses = {
|
96
|
+
'pending': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
97
|
+
'confirming': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
98
|
+
'completed': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
99
|
+
'confirmed': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
100
|
+
'failed': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
101
|
+
'cancelled': 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300',
|
102
|
+
'expired': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
|
103
|
+
'refunded': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300'
|
104
|
+
};
|
105
|
+
return statusClasses[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300';
|
106
|
+
},
|
107
|
+
|
108
|
+
async cancelPayment() {
|
109
|
+
if (!confirm('Are you sure you want to cancel this payment?')) {
|
110
|
+
return;
|
111
|
+
}
|
112
|
+
|
113
|
+
this.loading = true;
|
114
|
+
try {
|
115
|
+
const data = await PaymentAPI.admin.payments.cancel(this.paymentId);
|
116
|
+
PaymentAPI.utils.showNotification('Payment cancelled successfully', 'success');
|
117
|
+
|
118
|
+
// Update payment data
|
119
|
+
this.updatePaymentData(data);
|
120
|
+
} catch (error) {
|
121
|
+
console.error('Failed to cancel payment:', error);
|
122
|
+
PaymentAPI.utils.showNotification('Failed to cancel payment', 'error');
|
123
|
+
} finally {
|
124
|
+
this.loading = false;
|
125
|
+
}
|
126
|
+
},
|
127
|
+
|
128
|
+
exportDetails() {
|
129
|
+
// Get payment data from the page
|
130
|
+
const paymentData = {
|
131
|
+
id: this.paymentId,
|
132
|
+
status: document.querySelector('.payment-status-badge')?.textContent || 'Unknown',
|
133
|
+
amount_usd: document.querySelector('[data-field="amount_usd"]')?.textContent || '',
|
134
|
+
pay_amount: document.querySelector('[data-field="pay_amount"]')?.textContent || '',
|
135
|
+
currency: document.querySelector('[data-field="currency"]')?.textContent || '',
|
136
|
+
provider: document.querySelector('[data-field="provider"]')?.textContent || '',
|
137
|
+
pay_address: document.querySelector('[data-field="pay_address"]')?.textContent || '',
|
138
|
+
transaction_hash: document.querySelector('[data-field="transaction_hash"]')?.textContent || '',
|
139
|
+
created_at: document.querySelector('[data-field="created_at"]')?.textContent || '',
|
140
|
+
updated_at: document.querySelector('[data-field="updated_at"]')?.textContent || ''
|
141
|
+
};
|
142
|
+
|
143
|
+
// Create CSV content
|
144
|
+
const csvContent = Object.entries(paymentData)
|
145
|
+
.map(([key, value]) => `${key},${value}`)
|
146
|
+
.join('\n');
|
147
|
+
|
148
|
+
// Download CSV
|
149
|
+
const blob = new Blob([csvContent], { type: 'text/csv' });
|
150
|
+
const url = window.URL.createObjectURL(blob);
|
151
|
+
const a = document.createElement('a');
|
152
|
+
a.href = url;
|
153
|
+
a.download = `payment_${this.paymentId}_details.csv`;
|
154
|
+
document.body.appendChild(a);
|
155
|
+
a.click();
|
156
|
+
document.body.removeChild(a);
|
157
|
+
window.URL.revokeObjectURL(url);
|
158
|
+
}
|
159
|
+
};
|
160
|
+
}
|
161
|
+
|
162
|
+
// Auto-initialize if PaymentAPI is available
|
163
|
+
document.addEventListener('DOMContentLoaded', function() {
|
164
|
+
if (typeof PaymentAPI === 'undefined') {
|
165
|
+
console.warn('PaymentAPI not found. Make sure api-client.js is loaded before payment-detail.js');
|
166
|
+
}
|
167
|
+
});
|
@@ -22,6 +22,7 @@ function paymentForm() {
|
|
22
22
|
],
|
23
23
|
conversionResult: null,
|
24
24
|
users: [],
|
25
|
+
errorMessage: '',
|
25
26
|
|
26
27
|
async init() {
|
27
28
|
console.log('🚀 PaymentForm: Initializing...');
|
@@ -85,29 +86,20 @@ function paymentForm() {
|
|
85
86
|
return;
|
86
87
|
}
|
87
88
|
|
88
|
-
|
89
|
-
|
90
|
-
|
89
|
+
// Use the provider-currencies API endpoint directly
|
90
|
+
const response = await PaymentAPI.request(`/api/payments/provider-currencies/by_provider/?provider=${this.form.provider}`);
|
91
|
+
console.log('📊 Provider currencies API response:', response);
|
91
92
|
|
92
|
-
//
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
symbol: pc.currency?.symbol || '',
|
98
|
-
network: pc.network?.code || null,
|
99
|
-
network_name: pc.network?.name || null,
|
100
|
-
min_amount: pc.min_amount,
|
101
|
-
max_amount: pc.max_amount,
|
102
|
-
fee_percentage: pc.fee_percentage,
|
103
|
-
fixed_fee: pc.fixed_fee,
|
104
|
-
provider_code: pc.provider_currency_code
|
105
|
-
}));
|
93
|
+
// Extract currencies for the specific provider
|
94
|
+
const providerData = response.currencies_by_provider?.[this.form.provider];
|
95
|
+
this.currencies = providerData?.currencies || [];
|
96
|
+
|
97
|
+
console.log('🔍 Sample currency structure:', this.currencies[0]);
|
106
98
|
|
107
99
|
console.log('✅ Loaded provider currencies:', this.currencies.length);
|
108
100
|
|
109
101
|
// If current currency is not supported by provider, reset it
|
110
|
-
if (this.form.currency_code && !this.currencies.find(c => c.
|
102
|
+
if (this.form.currency_code && !this.currencies.find(c => c.provider_currency_code === this.form.currency_code)) {
|
111
103
|
console.log('⚠️ Current currency not supported by provider, resetting');
|
112
104
|
this.form.currency_code = '';
|
113
105
|
this.conversionResult = null;
|
@@ -179,11 +171,15 @@ function paymentForm() {
|
|
179
171
|
if (!this.form.amount_usd || !this.form.currency_code) return;
|
180
172
|
|
181
173
|
try {
|
182
|
-
|
174
|
+
// Get the original currency code for conversion (not provider code)
|
175
|
+
const currencyInfo = this.getCurrencyInfo(this.form.currency_code);
|
176
|
+
const originalCurrencyCode = currencyInfo.currency?.code || this.form.currency_code;
|
177
|
+
|
178
|
+
const result = await PaymentAPI.currencies.convert('USD', originalCurrencyCode, this.form.amount_usd);
|
183
179
|
this.conversionResult = {
|
184
180
|
amount: result.converted_amount,
|
185
181
|
rate: result.rate,
|
186
|
-
currency:
|
182
|
+
currency: originalCurrencyCode
|
187
183
|
};
|
188
184
|
} catch (error) {
|
189
185
|
console.error('Currency conversion failed:', error);
|
@@ -191,10 +187,10 @@ function paymentForm() {
|
|
191
187
|
}
|
192
188
|
},
|
193
189
|
|
194
|
-
getCurrencyInfo(
|
195
|
-
return this.currencies.find(c => c.
|
196
|
-
this.allCurrencies.find(c => c.code ===
|
197
|
-
{ code, name:
|
190
|
+
getCurrencyInfo(providerCurrencyCode) {
|
191
|
+
return this.currencies.find(c => c.provider_currency_code === providerCurrencyCode) ||
|
192
|
+
this.allCurrencies.find(c => c.code === providerCurrencyCode) ||
|
193
|
+
{ provider_currency_code: providerCurrencyCode, currency: { code: providerCurrencyCode, name: providerCurrencyCode, currency_type: 'unknown' } };
|
198
194
|
},
|
199
195
|
|
200
196
|
validateForm() {
|
@@ -208,10 +204,18 @@ function paymentForm() {
|
|
208
204
|
return errors;
|
209
205
|
},
|
210
206
|
|
207
|
+
clearError() {
|
208
|
+
this.errorMessage = '';
|
209
|
+
},
|
210
|
+
|
211
211
|
async submitForm() {
|
212
|
+
// Clear previous error
|
213
|
+
this.clearError();
|
214
|
+
|
212
215
|
const errors = this.validateForm();
|
213
216
|
if (errors.length > 0) {
|
214
|
-
|
217
|
+
this.errorMessage = errors.join(', ');
|
218
|
+
PaymentAPI.utils.showNotification('Please fix form errors', 'error');
|
215
219
|
return;
|
216
220
|
}
|
217
221
|
|
@@ -223,7 +227,12 @@ function paymentForm() {
|
|
223
227
|
window.location.href = `/cfg/admin/django_cfg_payments/admin/payments/${data.id}/`;
|
224
228
|
} catch (error) {
|
225
229
|
console.error('Error:', error);
|
226
|
-
|
230
|
+
|
231
|
+
// Show detailed error in form
|
232
|
+
this.errorMessage = error.message || 'Failed to create payment';
|
233
|
+
|
234
|
+
// Show brief notification
|
235
|
+
PaymentAPI.utils.showNotification('Payment creation failed', 'error');
|
227
236
|
} finally {
|
228
237
|
this.loading = false;
|
229
238
|
}
|
django_cfg/apps/payments/urls.py
CHANGED
@@ -11,7 +11,7 @@ from rest_framework_nested import routers
|
|
11
11
|
from .views.api import (
|
12
12
|
PaymentViewSet, UserPaymentViewSet, PaymentCreateView, PaymentStatusView,
|
13
13
|
UserBalanceViewSet, TransactionViewSet, UserTransactionViewSet,
|
14
|
-
CurrencyViewSet, NetworkViewSet, ProviderCurrencyViewSet,
|
14
|
+
CurrencyViewSet, NetworkViewSet, ProviderCurrencyViewSet, CurrencyRatesView, SupportedCurrenciesView,
|
15
15
|
SubscriptionViewSet, UserSubscriptionViewSet, EndpointGroupViewSet, TariffViewSet,
|
16
16
|
APIKeyViewSet, UserAPIKeyViewSet, APIKeyCreateView, APIKeyValidateView,
|
17
17
|
UniversalWebhookView, webhook_health_check, webhook_stats, supported_providers,
|
@@ -60,7 +60,8 @@ urlpatterns = [
|
|
60
60
|
path('payments/create/', PaymentCreateView.as_view(), name='payment-create'),
|
61
61
|
path('payments/status/<uuid:pk>/', PaymentStatusView.as_view(), name='payment-status'),
|
62
62
|
|
63
|
-
|
63
|
+
# Note: currencies/convert/ is handled by CurrencyViewSet action
|
64
|
+
# path('currencies/convert/', CurrencyConversionView.as_view(), name='currency-convert'),
|
64
65
|
path('currencies/rates/', CurrencyRatesView.as_view(), name='currency-rates'),
|
65
66
|
path('currencies/supported/', SupportedCurrenciesView.as_view(), name='currencies-supported'),
|
66
67
|
|
@@ -34,6 +34,9 @@ class CurrencyViewSet(ReadOnlyPaymentViewSet):
|
|
34
34
|
Read-only access to currency information with conversion capabilities.
|
35
35
|
"""
|
36
36
|
|
37
|
+
# Allow POST for conversion action
|
38
|
+
http_method_names = ['get', 'head', 'options', 'post']
|
39
|
+
|
37
40
|
queryset = Currency.objects.filter(is_active=True)
|
38
41
|
serializer_class = CurrencySerializer
|
39
42
|
permission_classes = [permissions.IsAuthenticated]
|
@@ -165,14 +165,18 @@ class CurrencyConversionSerializer(serializers.Serializer):
|
|
165
165
|
def save(self) -> Dict[str, Any]:
|
166
166
|
"""Perform currency conversion using CurrencyService."""
|
167
167
|
try:
|
168
|
-
|
169
|
-
|
168
|
+
from django_cfg.apps.payments.services.types.requests import CurrencyConversionRequest
|
169
|
+
|
170
|
+
# Create request object
|
171
|
+
request = CurrencyConversionRequest(
|
170
172
|
from_currency=self.validated_data['from_currency'],
|
171
173
|
to_currency=self.validated_data['to_currency'],
|
172
|
-
amount=self.validated_data['amount']
|
173
|
-
provider=self.validated_data.get('provider', 'nowpayments')
|
174
|
+
amount=self.validated_data['amount']
|
174
175
|
)
|
175
176
|
|
177
|
+
currency_service = get_currency_service()
|
178
|
+
result = currency_service.convert_currency(request)
|
179
|
+
|
176
180
|
if result.success:
|
177
181
|
return {
|
178
182
|
'success': True,
|
@@ -311,9 +315,18 @@ class SupportedCurrenciesSerializer(serializers.Serializer):
|
|
311
315
|
)
|
312
316
|
|
313
317
|
if result.success:
|
318
|
+
# Extract currencies from result.data structure
|
319
|
+
currencies_data = result.data.get('currencies', []) if isinstance(result.data, dict) else []
|
320
|
+
count = result.data.get('count', 0) if isinstance(result.data, dict) else len(currencies_data)
|
321
|
+
provider = result.data.get('provider', 'nowpayments') if isinstance(result.data, dict) else 'nowpayments'
|
322
|
+
|
314
323
|
return {
|
315
324
|
'success': True,
|
316
|
-
'currencies':
|
325
|
+
'currencies': {
|
326
|
+
'currencies': currencies_data,
|
327
|
+
'count': count,
|
328
|
+
'provider': provider
|
329
|
+
},
|
317
330
|
'message': result.message
|
318
331
|
}
|
319
332
|
else:
|
@@ -58,8 +58,8 @@ if DRAMATIQ_AVAILABLE:
|
|
58
58
|
class TaskQueueAdminMixin(OptimizedModelAdmin, DisplayMixin, StandaloneActionsMixin):
|
59
59
|
"""Mixin for task queue management functionality."""
|
60
60
|
|
61
|
-
change_list_template = 'admin/tasks/taskqueue/change_list.html'
|
62
|
-
actions_list = ['start_workers', 'clear_queues', 'refresh_status']
|
61
|
+
# change_list_template = 'admin/tasks/taskqueue/change_list.html'
|
62
|
+
# actions_list = ['start_workers', 'clear_queues', 'refresh_status']
|
63
63
|
|
64
64
|
def has_add_permission(self, request):
|
65
65
|
return False
|