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
@@ -16,6 +16,7 @@ from ..types import (
|
|
16
16
|
PaymentCreateRequest, PaymentStatusRequest, PaymentResult,
|
17
17
|
PaymentData, ServiceOperationResult
|
18
18
|
)
|
19
|
+
from ...models.managers.payment_managers import PaymentStatusUpdateFields
|
19
20
|
from ...models import UniversalPayment, Currency, ProviderCurrency
|
20
21
|
from ..providers import ProviderRegistry, get_provider_registry
|
21
22
|
|
@@ -49,12 +50,6 @@ class PaymentService(BaseService):
|
|
49
50
|
if isinstance(request, dict):
|
50
51
|
request = PaymentCreateRequest(**request)
|
51
52
|
|
52
|
-
self.logger.info("Creating payment", extra={
|
53
|
-
'user_id': request.user_id,
|
54
|
-
'amount_usd': request.amount_usd,
|
55
|
-
'currency_code': request.currency_code,
|
56
|
-
'provider': request.provider
|
57
|
-
})
|
58
53
|
|
59
54
|
# Get user
|
60
55
|
try:
|
@@ -94,6 +89,7 @@ class PaymentService(BaseService):
|
|
94
89
|
network=currency.native_networks.first(), # Use first native network
|
95
90
|
provider=request.provider,
|
96
91
|
status=UniversalPayment.PaymentStatus.PENDING,
|
92
|
+
status_changed_at=timezone.now(), # Track initial status setting
|
97
93
|
callback_url=request.callback_url,
|
98
94
|
cancel_url=request.cancel_url,
|
99
95
|
description=request.description,
|
@@ -103,12 +99,16 @@ class PaymentService(BaseService):
|
|
103
99
|
|
104
100
|
payment = self._execute_with_transaction(create_payment_transaction)
|
105
101
|
|
102
|
+
|
106
103
|
# Create payment with provider
|
107
104
|
from ..providers.models import PaymentRequest as ProviderPaymentRequest
|
108
105
|
|
106
|
+
# Use provider_currency_code from metadata if available, otherwise use original currency_code
|
107
|
+
provider_currency_code = request.metadata.get('provider_currency_code', request.currency_code)
|
108
|
+
|
109
109
|
provider_request = ProviderPaymentRequest(
|
110
110
|
amount_usd=request.amount_usd,
|
111
|
-
currency_code=
|
111
|
+
currency_code=provider_currency_code, # Use provider-specific currency code
|
112
112
|
order_id=str(payment.id),
|
113
113
|
callback_url=request.callback_url,
|
114
114
|
cancel_url=request.cancel_url,
|
@@ -118,50 +118,75 @@ class PaymentService(BaseService):
|
|
118
118
|
|
119
119
|
provider_response = provider.create_payment(provider_request)
|
120
120
|
|
121
|
+
|
121
122
|
# Update payment with provider response
|
122
123
|
if provider_response.success:
|
123
124
|
def update_payment_transaction():
|
124
125
|
payment.provider_payment_id = provider_response.provider_payment_id
|
125
|
-
payment.
|
126
|
+
payment.pay_amount = provider_response.amount # Fix: use pay_amount instead of crypto_amount
|
126
127
|
payment.payment_url = provider_response.payment_url
|
127
128
|
payment.qr_code_url = provider_response.qr_code_url
|
128
|
-
payment.
|
129
|
+
payment.pay_address = provider_response.wallet_address # Fix: use pay_address instead of wallet_address
|
129
130
|
if provider_response.expires_at:
|
130
131
|
payment.expires_at = provider_response.expires_at
|
131
132
|
payment.save()
|
132
133
|
return payment
|
133
134
|
|
134
135
|
payment = self._execute_with_transaction(update_payment_transaction)
|
136
|
+
|
137
|
+
# Convert to PaymentData using our helper method
|
138
|
+
payment_data = self._convert_payment_to_data(payment)
|
139
|
+
|
140
|
+
self._log_operation(
|
141
|
+
"create_payment",
|
142
|
+
True,
|
143
|
+
payment_id=str(payment.id),
|
144
|
+
user_id=request.user_id,
|
145
|
+
amount_usd=request.amount_usd
|
146
|
+
)
|
147
|
+
|
148
|
+
return PaymentResult(
|
149
|
+
success=True,
|
150
|
+
message="Payment created successfully",
|
151
|
+
payment_id=str(payment.id),
|
152
|
+
status=payment.status,
|
153
|
+
amount_usd=payment.amount_usd,
|
154
|
+
crypto_amount=payment.pay_amount,
|
155
|
+
currency_code=payment.currency.code,
|
156
|
+
payment_url=payment.payment_url,
|
157
|
+
expires_at=payment.expires_at,
|
158
|
+
data={'payment': payment_data.model_dump()}
|
159
|
+
)
|
160
|
+
|
135
161
|
else:
|
136
162
|
# Mark payment as failed if provider creation failed
|
163
|
+
self.logger.error("❌ PAYMENT SERVICE: Provider creation failed", extra={
|
164
|
+
'payment_id': str(payment.id),
|
165
|
+
'provider_error': getattr(provider_response, 'error_message', 'Unknown error')
|
166
|
+
})
|
137
167
|
payment.mark_failed(
|
138
168
|
reason=provider_response.error_message,
|
139
169
|
error_code="provider_creation_failed"
|
140
170
|
)
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
currency_code=payment.currency.code,
|
161
|
-
payment_url=payment.payment_url,
|
162
|
-
expires_at=payment.expires_at,
|
163
|
-
data={'payment': payment_data.model_dump()}
|
164
|
-
)
|
171
|
+
|
172
|
+
# Return error result when provider fails
|
173
|
+
self._log_operation(
|
174
|
+
"create_payment",
|
175
|
+
False,
|
176
|
+
payment_id=str(payment.id),
|
177
|
+
user_id=request.user_id,
|
178
|
+
amount_usd=request.amount_usd,
|
179
|
+
error=provider_response.error_message
|
180
|
+
)
|
181
|
+
|
182
|
+
return PaymentResult(
|
183
|
+
success=False,
|
184
|
+
message=provider_response.error_message or "Payment creation failed",
|
185
|
+
error_code="provider_creation_failed",
|
186
|
+
payment_id=str(payment.id),
|
187
|
+
status=payment.status,
|
188
|
+
data={'error_details': getattr(provider_response, 'raw_response', {})}
|
189
|
+
)
|
165
190
|
|
166
191
|
except Exception as e:
|
167
192
|
return PaymentResult(**self._handle_exception(
|
@@ -362,21 +387,114 @@ class PaymentService(BaseService):
|
|
362
387
|
)
|
363
388
|
|
364
389
|
def _check_provider_status(self, payment: UniversalPayment) -> ServiceOperationResult:
|
365
|
-
"""Check payment status with provider."""
|
390
|
+
"""Check payment status with provider and update database if needed."""
|
366
391
|
try:
|
367
|
-
|
368
|
-
|
392
|
+
if not payment.provider_payment_id:
|
393
|
+
return self._create_success_result(
|
394
|
+
"No provider payment ID, skipping provider check",
|
395
|
+
{'status_changed': False}
|
396
|
+
)
|
397
|
+
|
398
|
+
# Get provider instance
|
399
|
+
provider = self.provider_registry.get_provider(payment.provider)
|
400
|
+
if not provider:
|
401
|
+
return self._create_error_result(
|
402
|
+
f"Provider {payment.provider} not available",
|
403
|
+
"provider_not_available"
|
404
|
+
)
|
405
|
+
|
406
|
+
# Check status with provider
|
407
|
+
provider_response = provider.get_payment_status(payment.provider_payment_id)
|
408
|
+
|
409
|
+
if not provider_response.success:
|
410
|
+
self.logger.warning(f"Provider status check failed for payment {payment.id}", extra={
|
411
|
+
'payment_id': str(payment.id),
|
412
|
+
'provider_payment_id': payment.provider_payment_id,
|
413
|
+
'error': provider_response.error_message
|
414
|
+
})
|
415
|
+
return self._create_error_result(
|
416
|
+
f"Provider status check failed: {provider_response.error_message}",
|
417
|
+
"provider_check_failed"
|
418
|
+
)
|
419
|
+
|
420
|
+
# Map provider status to our status
|
421
|
+
original_status = payment.status
|
422
|
+
new_status = self._map_provider_status(provider_response.status, payment.provider)
|
423
|
+
|
424
|
+
status_changed = False
|
425
|
+
|
426
|
+
# Update payment if status changed
|
427
|
+
if new_status and new_status != original_status:
|
428
|
+
def update_payment_transaction():
|
429
|
+
nonlocal status_changed
|
430
|
+
|
431
|
+
# Update status
|
432
|
+
payment.status = new_status
|
433
|
+
payment.status_changed_at = timezone.now() # Track when status changed
|
434
|
+
|
435
|
+
# Update other fields from provider response
|
436
|
+
if provider_response.transaction_hash and not payment.transaction_hash:
|
437
|
+
payment.transaction_hash = provider_response.transaction_hash
|
438
|
+
|
439
|
+
if provider_response.confirmations_count is not None:
|
440
|
+
payment.confirmations_count = provider_response.confirmations_count
|
441
|
+
|
442
|
+
# Mark as completed if status is completed/confirmed
|
443
|
+
if new_status in ['completed', 'confirmed'] and not payment.completed_at:
|
444
|
+
payment.completed_at = timezone.now()
|
445
|
+
|
446
|
+
payment.save()
|
447
|
+
status_changed = True
|
448
|
+
|
449
|
+
return True
|
450
|
+
|
451
|
+
# Execute in transaction
|
452
|
+
self._execute_with_transaction(update_payment_transaction)
|
453
|
+
|
454
|
+
self.logger.info(f"Payment status updated from provider", extra={
|
455
|
+
'payment_id': str(payment.id),
|
456
|
+
'provider_payment_id': payment.provider_payment_id,
|
457
|
+
'status_change': f"{original_status} -> {new_status}",
|
458
|
+
'provider': payment.provider
|
459
|
+
})
|
460
|
+
|
369
461
|
return self._create_success_result(
|
370
|
-
"Provider status checked",
|
371
|
-
{
|
462
|
+
f"Provider status checked: {original_status} -> {new_status}" if status_changed else "No status change",
|
463
|
+
{
|
464
|
+
'status_changed': status_changed,
|
465
|
+
'original_status': original_status,
|
466
|
+
'new_status': new_status,
|
467
|
+
'provider_response': provider_response.raw_response
|
468
|
+
}
|
372
469
|
)
|
373
470
|
|
374
471
|
except Exception as e:
|
472
|
+
self.logger.error(f"Provider status check failed", extra={
|
473
|
+
'payment_id': str(payment.id),
|
474
|
+
'error': str(e)
|
475
|
+
})
|
375
476
|
return self._create_error_result(
|
376
477
|
f"Provider check failed: {e}",
|
377
478
|
"provider_check_failed"
|
378
479
|
)
|
379
480
|
|
481
|
+
def _map_provider_status(self, provider_status: str, provider: str) -> Optional[str]:
|
482
|
+
"""Map provider-specific status to universal status."""
|
483
|
+
if provider == 'nowpayments':
|
484
|
+
status_mapping = {
|
485
|
+
'waiting': 'pending',
|
486
|
+
'confirming': 'confirming',
|
487
|
+
'confirmed': 'confirmed',
|
488
|
+
'finished': 'completed',
|
489
|
+
'failed': 'failed',
|
490
|
+
'refunded': 'refunded',
|
491
|
+
'expired': 'expired'
|
492
|
+
}
|
493
|
+
return status_mapping.get(provider_status.lower())
|
494
|
+
|
495
|
+
# Default mapping for unknown providers
|
496
|
+
return provider_status.lower() if provider_status else None
|
497
|
+
|
380
498
|
def get_user_payments(
|
381
499
|
self,
|
382
500
|
user_id: int,
|
@@ -477,3 +595,112 @@ class PaymentService(BaseService):
|
|
477
595
|
|
478
596
|
except Exception as e:
|
479
597
|
return self._handle_exception("get_payment_stats", e)
|
598
|
+
|
599
|
+
def _check_provider_status(self, payment: 'UniversalPayment') -> ServiceOperationResult:
|
600
|
+
"""
|
601
|
+
Check payment status with provider and update if changed.
|
602
|
+
|
603
|
+
Args:
|
604
|
+
payment: Payment object to check
|
605
|
+
|
606
|
+
Returns:
|
607
|
+
ServiceOperationResult: Result with status_changed flag
|
608
|
+
"""
|
609
|
+
try:
|
610
|
+
from ..providers.registry import ProviderRegistry
|
611
|
+
from django.db import transaction
|
612
|
+
|
613
|
+
self.logger.debug("Checking provider status", extra={
|
614
|
+
'payment_id': str(payment.id),
|
615
|
+
'current_status': payment.status,
|
616
|
+
'provider': payment.provider
|
617
|
+
})
|
618
|
+
|
619
|
+
# Get provider instance
|
620
|
+
registry = ProviderRegistry()
|
621
|
+
provider = registry.get_provider(payment.provider)
|
622
|
+
|
623
|
+
if not provider:
|
624
|
+
return self._create_error_result(
|
625
|
+
f"Provider {payment.provider} not found",
|
626
|
+
"provider_not_found"
|
627
|
+
)
|
628
|
+
|
629
|
+
# Get status from provider
|
630
|
+
provider_response = provider.get_payment_status(payment.provider_payment_id)
|
631
|
+
|
632
|
+
if not provider_response.success:
|
633
|
+
self.logger.warning("Provider status check failed", extra={
|
634
|
+
'payment_id': str(payment.id),
|
635
|
+
'provider': payment.provider,
|
636
|
+
'error': provider_response.error_message
|
637
|
+
})
|
638
|
+
return self._create_error_result(
|
639
|
+
f"Provider status check failed: {provider_response.error_message}",
|
640
|
+
"provider_check_failed"
|
641
|
+
)
|
642
|
+
|
643
|
+
# Map provider status to universal status
|
644
|
+
provider_status = provider_response.data.get('status', '').lower()
|
645
|
+
status_mapping = {
|
646
|
+
'waiting': UniversalPayment.PaymentStatus.PENDING,
|
647
|
+
'confirming': UniversalPayment.PaymentStatus.PENDING,
|
648
|
+
'confirmed': UniversalPayment.PaymentStatus.COMPLETED,
|
649
|
+
'sending': UniversalPayment.PaymentStatus.PENDING,
|
650
|
+
'partially_paid': UniversalPayment.PaymentStatus.PENDING,
|
651
|
+
'finished': UniversalPayment.PaymentStatus.COMPLETED,
|
652
|
+
'failed': UniversalPayment.PaymentStatus.FAILED,
|
653
|
+
'refunded': UniversalPayment.PaymentStatus.FAILED,
|
654
|
+
'expired': UniversalPayment.PaymentStatus.EXPIRED,
|
655
|
+
}
|
656
|
+
|
657
|
+
new_status = status_mapping.get(provider_status, payment.status)
|
658
|
+
status_changed = new_status != payment.status
|
659
|
+
|
660
|
+
# Update payment if status changed
|
661
|
+
if status_changed:
|
662
|
+
with transaction.atomic():
|
663
|
+
# Prepare extra fields from provider response
|
664
|
+
provider_data = provider_response.data
|
665
|
+
|
666
|
+
extra_fields = PaymentStatusUpdateFields(
|
667
|
+
transaction_hash=provider_data.get('transaction_hash'),
|
668
|
+
confirmations_count=provider_data.get('confirmations_count')
|
669
|
+
)
|
670
|
+
|
671
|
+
# Use manager method for consistent status updates
|
672
|
+
from ...models import UniversalPayment
|
673
|
+
success = UniversalPayment.objects.update_payment_status(
|
674
|
+
payment, new_status, extra_fields
|
675
|
+
)
|
676
|
+
|
677
|
+
if not success:
|
678
|
+
return self._create_error_result(
|
679
|
+
"Failed to update payment status",
|
680
|
+
"status_update_failed"
|
681
|
+
)
|
682
|
+
|
683
|
+
self.logger.info("Payment status updated", extra={
|
684
|
+
'payment_id': str(payment.id),
|
685
|
+
'old_status': payment.status,
|
686
|
+
'new_status': new_status,
|
687
|
+
'provider_status': provider_status
|
688
|
+
})
|
689
|
+
|
690
|
+
return self._create_success_result(
|
691
|
+
"Provider status checked",
|
692
|
+
{
|
693
|
+
'status_changed': status_changed,
|
694
|
+
'old_status': payment.status if not status_changed else None,
|
695
|
+
'new_status': new_status,
|
696
|
+
'provider_status': provider_status,
|
697
|
+
'provider_response': provider_response.data
|
698
|
+
}
|
699
|
+
)
|
700
|
+
|
701
|
+
except Exception as e:
|
702
|
+
self.logger.error("Error checking provider status", extra={
|
703
|
+
'payment_id': str(payment.id),
|
704
|
+
'error': str(e)
|
705
|
+
})
|
706
|
+
return self._handle_exception("_check_provider_status", e)
|
@@ -7,9 +7,11 @@ Abstract base class for all payment providers with unified interface.
|
|
7
7
|
from abc import ABC, abstractmethod
|
8
8
|
from typing import Dict, Any, Optional
|
9
9
|
from decimal import Decimal
|
10
|
+
import requests
|
11
|
+
from django.utils import timezone
|
10
12
|
from django_cfg.modules.django_logger import get_logger
|
11
13
|
from ..types import ProviderResponse, ServiceOperationResult
|
12
|
-
from .models import ProviderConfig, PaymentRequest
|
14
|
+
from .models import ProviderConfig, PaymentRequest, WithdrawalRequest
|
13
15
|
|
14
16
|
|
15
17
|
class BaseProvider(ABC):
|
@@ -38,7 +40,7 @@ class BaseProvider(ABC):
|
|
38
40
|
@property
|
39
41
|
def is_sandbox(self) -> bool:
|
40
42
|
"""Check if provider is in sandbox mode."""
|
41
|
-
return self.config.
|
43
|
+
return self.config.sandbox_mode
|
42
44
|
|
43
45
|
# Provider configuration methods (to be overridden by specific providers)
|
44
46
|
def get_fee_percentage(self, currency_code: str = None, currency_type: str = None) -> Decimal:
|
@@ -149,6 +151,61 @@ class BaseProvider(ABC):
|
|
149
151
|
"""
|
150
152
|
pass
|
151
153
|
|
154
|
+
def refresh_payment_status(self, provider_payment_id: str, force_update: bool = True) -> ProviderResponse:
|
155
|
+
"""
|
156
|
+
Refresh payment status with enhanced error handling and caching control.
|
157
|
+
|
158
|
+
This method provides additional functionality over get_payment_status:
|
159
|
+
- Enhanced error handling
|
160
|
+
- Optional caching control
|
161
|
+
- Detailed logging
|
162
|
+
|
163
|
+
Args:
|
164
|
+
provider_payment_id: Provider's payment ID
|
165
|
+
force_update: Whether to bypass cache and force fresh data
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
ProviderResponse: Current payment status with enhanced metadata
|
169
|
+
"""
|
170
|
+
self.logger.info(f"Refreshing payment status for {provider_payment_id}", extra={
|
171
|
+
'provider': self.name,
|
172
|
+
'payment_id': provider_payment_id,
|
173
|
+
'force_update': force_update
|
174
|
+
})
|
175
|
+
|
176
|
+
try:
|
177
|
+
# Call the provider-specific implementation
|
178
|
+
result = self.get_payment_status(provider_payment_id)
|
179
|
+
|
180
|
+
# Add refresh metadata
|
181
|
+
if result.success and hasattr(result, 'raw_response') and result.raw_response:
|
182
|
+
result.raw_response['_refresh_metadata'] = {
|
183
|
+
'refreshed_at': timezone.now().isoformat(),
|
184
|
+
'provider': self.name,
|
185
|
+
'force_update': force_update
|
186
|
+
}
|
187
|
+
|
188
|
+
self.logger.info(f"Payment status refreshed successfully", extra={
|
189
|
+
'provider': self.name,
|
190
|
+
'payment_id': provider_payment_id,
|
191
|
+
'status': getattr(result, 'status', 'unknown')
|
192
|
+
})
|
193
|
+
|
194
|
+
return result
|
195
|
+
|
196
|
+
except Exception as e:
|
197
|
+
self.logger.error(f"Failed to refresh payment status", extra={
|
198
|
+
'provider': self.name,
|
199
|
+
'payment_id': provider_payment_id,
|
200
|
+
'error': str(e)
|
201
|
+
})
|
202
|
+
|
203
|
+
return ProviderResponse(
|
204
|
+
success=False,
|
205
|
+
error_message=f"Failed to refresh payment status: {str(e)}",
|
206
|
+
raw_response={'error': str(e), 'provider': self.name}
|
207
|
+
)
|
208
|
+
|
152
209
|
@abstractmethod
|
153
210
|
def get_supported_currencies(self) -> ServiceOperationResult:
|
154
211
|
"""
|
@@ -183,6 +240,129 @@ class BaseProvider(ABC):
|
|
183
240
|
"""
|
184
241
|
pass
|
185
242
|
|
243
|
+
# Withdrawal/Payout Methods
|
244
|
+
|
245
|
+
def supports_withdrawals(self) -> bool:
|
246
|
+
"""
|
247
|
+
Check if provider supports withdrawals/payouts.
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
bool: True if provider supports withdrawals
|
251
|
+
"""
|
252
|
+
return False # Default: most payment processors don't support withdrawals
|
253
|
+
|
254
|
+
def create_withdrawal(self, request: WithdrawalRequest) -> ProviderResponse:
|
255
|
+
"""
|
256
|
+
Create withdrawal/payout request with provider.
|
257
|
+
|
258
|
+
Args:
|
259
|
+
request: Withdrawal creation request
|
260
|
+
|
261
|
+
Returns:
|
262
|
+
ProviderResponse: Provider response with withdrawal details
|
263
|
+
|
264
|
+
Raises:
|
265
|
+
NotImplementedError: If provider doesn't support withdrawals
|
266
|
+
"""
|
267
|
+
if not self.supports_withdrawals():
|
268
|
+
return ProviderResponse(
|
269
|
+
success=False,
|
270
|
+
error_message=f"Provider {self.name} does not support withdrawals",
|
271
|
+
raw_response={'error': 'withdrawals_not_supported'}
|
272
|
+
)
|
273
|
+
|
274
|
+
raise NotImplementedError("Subclasses must implement create_withdrawal if they support withdrawals")
|
275
|
+
|
276
|
+
def get_withdrawal_status(self, provider_withdrawal_id: str) -> ProviderResponse:
|
277
|
+
"""
|
278
|
+
Get withdrawal status from provider.
|
279
|
+
|
280
|
+
Args:
|
281
|
+
provider_withdrawal_id: Provider's withdrawal ID
|
282
|
+
|
283
|
+
Returns:
|
284
|
+
ProviderResponse: Current withdrawal status
|
285
|
+
|
286
|
+
Raises:
|
287
|
+
NotImplementedError: If provider doesn't support withdrawals
|
288
|
+
"""
|
289
|
+
if not self.supports_withdrawals():
|
290
|
+
return ProviderResponse(
|
291
|
+
success=False,
|
292
|
+
error_message=f"Provider {self.name} does not support withdrawals",
|
293
|
+
raw_response={'error': 'withdrawals_not_supported'}
|
294
|
+
)
|
295
|
+
|
296
|
+
raise NotImplementedError("Subclasses must implement get_withdrawal_status if they support withdrawals")
|
297
|
+
|
298
|
+
def cancel_withdrawal(self, provider_withdrawal_id: str) -> ProviderResponse:
|
299
|
+
"""
|
300
|
+
Cancel pending withdrawal.
|
301
|
+
|
302
|
+
Args:
|
303
|
+
provider_withdrawal_id: Provider's withdrawal ID
|
304
|
+
|
305
|
+
Returns:
|
306
|
+
ProviderResponse: Cancellation result
|
307
|
+
|
308
|
+
Raises:
|
309
|
+
NotImplementedError: If provider doesn't support withdrawal cancellation
|
310
|
+
"""
|
311
|
+
if not self.supports_withdrawals():
|
312
|
+
return ProviderResponse(
|
313
|
+
success=False,
|
314
|
+
error_message=f"Provider {self.name} does not support withdrawals",
|
315
|
+
raw_response={'error': 'withdrawals_not_supported'}
|
316
|
+
)
|
317
|
+
|
318
|
+
return ProviderResponse(
|
319
|
+
success=False,
|
320
|
+
error_message=f"Provider {self.name} does not support withdrawal cancellation",
|
321
|
+
raw_response={'error': 'withdrawal_cancellation_not_supported'}
|
322
|
+
)
|
323
|
+
|
324
|
+
def get_withdrawal_fees(self, currency_code: str) -> ServiceOperationResult:
|
325
|
+
"""
|
326
|
+
Get withdrawal fees for specific currency.
|
327
|
+
|
328
|
+
Args:
|
329
|
+
currency_code: Currency code
|
330
|
+
|
331
|
+
Returns:
|
332
|
+
ServiceOperationResult: Fee information or not supported
|
333
|
+
"""
|
334
|
+
if not self.supports_withdrawals():
|
335
|
+
return ServiceOperationResult(
|
336
|
+
success=False,
|
337
|
+
message=f"Provider {self.name} does not support withdrawals"
|
338
|
+
)
|
339
|
+
|
340
|
+
return ServiceOperationResult(
|
341
|
+
success=False,
|
342
|
+
message="Withdrawal fee information not available"
|
343
|
+
)
|
344
|
+
|
345
|
+
def get_minimum_withdrawal_amount(self, currency_code: str) -> ServiceOperationResult:
|
346
|
+
"""
|
347
|
+
Get minimum withdrawal amount for specific currency.
|
348
|
+
|
349
|
+
Args:
|
350
|
+
currency_code: Currency code
|
351
|
+
|
352
|
+
Returns:
|
353
|
+
ServiceOperationResult: Minimum amount or not supported
|
354
|
+
"""
|
355
|
+
if not self.supports_withdrawals():
|
356
|
+
return ServiceOperationResult(
|
357
|
+
success=False,
|
358
|
+
message=f"Provider {self.name} does not support withdrawals"
|
359
|
+
)
|
360
|
+
|
361
|
+
return ServiceOperationResult(
|
362
|
+
success=False,
|
363
|
+
message="Minimum withdrawal amount information not available"
|
364
|
+
)
|
365
|
+
|
186
366
|
def get_exchange_rate(self, from_currency: str, to_currency: str) -> ServiceOperationResult:
|
187
367
|
"""
|
188
368
|
Get exchange rate from provider (optional).
|
@@ -313,7 +493,33 @@ class BaseProvider(ABC):
|
|
313
493
|
})
|
314
494
|
|
315
495
|
# Handle response
|
316
|
-
|
496
|
+
try:
|
497
|
+
response.raise_for_status()
|
498
|
+
except requests.exceptions.HTTPError as e:
|
499
|
+
# Log the error with response content for debugging
|
500
|
+
error_content = response.text if response.text else "No response content"
|
501
|
+
self.logger.error(f"HTTP {response.status_code} error from {self.name}", extra={
|
502
|
+
'status_code': response.status_code,
|
503
|
+
'error_content': error_content[:500], # Limit content length
|
504
|
+
'url': response.url
|
505
|
+
})
|
506
|
+
|
507
|
+
# Handle specific HTTP errors
|
508
|
+
if response.status_code == 403:
|
509
|
+
if "blocked" in error_content.lower() or "nowpayments.io" in error_content:
|
510
|
+
raise Exception(f"IP address blocked by {self.name}. Please contact {self.name} support or use a different IP/VPN.")
|
511
|
+
else:
|
512
|
+
raise Exception(f"Access forbidden by {self.name}. Check API key and permissions.")
|
513
|
+
elif response.status_code == 401:
|
514
|
+
raise Exception(f"Authentication failed with {self.name}. Check API key.")
|
515
|
+
elif response.status_code == 400:
|
516
|
+
raise Exception(f"Bad request to {self.name}: {error_content[:200]}")
|
517
|
+
elif response.status_code == 429:
|
518
|
+
raise Exception(f"Rate limit exceeded for {self.name}. Please try again later.")
|
519
|
+
elif response.status_code >= 500:
|
520
|
+
raise Exception(f"{self.name} server error ({response.status_code}). Please try again later.")
|
521
|
+
else:
|
522
|
+
raise Exception(f"HTTP {response.status_code} error from {self.name}: {error_content[:200]}")
|
317
523
|
|
318
524
|
try:
|
319
525
|
return response.json()
|
@@ -7,6 +7,7 @@ Common models used across all payment providers.
|
|
7
7
|
from .base import (
|
8
8
|
ProviderConfig,
|
9
9
|
PaymentRequest,
|
10
|
+
WithdrawalRequest,
|
10
11
|
ProviderMetadata,
|
11
12
|
ProviderType,
|
12
13
|
ProviderStatus
|
@@ -25,6 +26,7 @@ __all__ = [
|
|
25
26
|
# Base models
|
26
27
|
'ProviderConfig',
|
27
28
|
'PaymentRequest',
|
29
|
+
'WithdrawalRequest',
|
28
30
|
'ProviderMetadata',
|
29
31
|
'ProviderType',
|
30
32
|
'ProviderStatus',
|
@@ -44,8 +44,7 @@ class ProviderConfig(BaseModel):
|
|
44
44
|
api_key: str = Field(..., description="Provider API key")
|
45
45
|
api_url: str = Field(..., description="Provider API URL")
|
46
46
|
enabled: bool = Field(default=True, description="Whether provider is enabled")
|
47
|
-
|
48
|
-
sandbox_mode: bool = Field(default=False, description="Sandbox mode (alias for sandbox)")
|
47
|
+
sandbox_mode: bool = Field(default=False, description="Sandbox mode")
|
49
48
|
timeout: int = Field(default=30, ge=5, le=300, description="Request timeout in seconds")
|
50
49
|
retry_attempts: int = Field(default=3, ge=0, le=10, description="Number of retry attempts")
|
51
50
|
retry_delay: int = Field(default=5, ge=1, le=60, description="Delay between retries in seconds")
|
@@ -77,6 +76,30 @@ class PaymentRequest(BaseModel):
|
|
77
76
|
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
78
77
|
|
79
78
|
|
79
|
+
class WithdrawalRequest(BaseModel):
|
80
|
+
"""
|
81
|
+
Universal withdrawal request for providers with Pydantic v2.
|
82
|
+
|
83
|
+
Standardized withdrawal/payout request across all providers.
|
84
|
+
"""
|
85
|
+
model_config = ConfigDict(
|
86
|
+
validate_assignment=True,
|
87
|
+
extra="forbid",
|
88
|
+
str_strip_whitespace=True
|
89
|
+
)
|
90
|
+
|
91
|
+
amount: float = Field(..., gt=0, description="Withdrawal amount in crypto currency")
|
92
|
+
currency_code: str = Field(..., min_length=3, max_length=10, description="Cryptocurrency code")
|
93
|
+
destination_address: str = Field(..., min_length=10, description="Destination wallet address")
|
94
|
+
withdrawal_id: str = Field(..., min_length=1, max_length=100, description="Internal withdrawal ID")
|
95
|
+
callback_url: Optional[str] = Field(None, description="Withdrawal status callback URL")
|
96
|
+
description: Optional[str] = Field(None, max_length=500, description="Withdrawal description")
|
97
|
+
extra_id: Optional[str] = Field(None, description="Extra ID for destination (memo, tag, etc.)")
|
98
|
+
priority: Optional[str] = Field(default="normal", description="Transaction priority (low, normal, high)")
|
99
|
+
|
100
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
101
|
+
|
102
|
+
|
80
103
|
class ProviderMetadata(BaseModel):
|
81
104
|
"""
|
82
105
|
Provider metadata with classification and features.
|