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.
Files changed (187) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin/networks_admin.py +12 -1
  3. django_cfg/apps/payments/admin/payments_admin.py +13 -0
  4. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +62 -14
  5. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
  6. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
  7. django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
  8. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
  9. django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
  10. django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
  11. django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
  12. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +33 -3
  13. django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
  14. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +96 -45
  15. django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
  16. django_cfg/apps/payments/config/__init__.py +14 -15
  17. django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
  18. django_cfg/apps/payments/config/helpers.py +8 -13
  19. django_cfg/apps/payments/migrations/0001_initial.py +33 -46
  20. django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
  21. django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
  22. django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
  23. django_cfg/apps/payments/models/payments.py +94 -0
  24. django_cfg/apps/payments/services/core/base.py +4 -4
  25. django_cfg/apps/payments/services/core/payment_service.py +265 -38
  26. django_cfg/apps/payments/services/providers/base.py +209 -3
  27. django_cfg/apps/payments/services/providers/models/__init__.py +2 -0
  28. django_cfg/apps/payments/services/providers/models/base.py +25 -2
  29. django_cfg/apps/payments/services/providers/nowpayments/models.py +2 -2
  30. django_cfg/apps/payments/services/providers/nowpayments/provider.py +57 -9
  31. django_cfg/apps/payments/services/providers/registry.py +5 -5
  32. django_cfg/apps/payments/services/types/requests.py +19 -7
  33. django_cfg/apps/payments/signals/payment_signals.py +31 -2
  34. django_cfg/apps/payments/static/payments/js/api-client.js +6 -1
  35. django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
  36. django_cfg/apps/payments/static/payments/js/payment-form.js +35 -26
  37. django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
  38. django_cfg/apps/payments/urls.py +3 -2
  39. django_cfg/apps/payments/views/api/currencies.py +3 -0
  40. django_cfg/apps/payments/views/serializers/currencies.py +18 -5
  41. django_cfg/apps/tasks/admin/tasks_admin.py +2 -2
  42. django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
  43. django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
  44. django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
  45. django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
  46. django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
  47. django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
  48. django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
  49. django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
  50. django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
  51. django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
  52. django_cfg/apps/tasks/tasks/__init__.py +10 -0
  53. django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
  54. django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
  55. django_cfg/apps/tasks/templates/tasks/components/{status_cards.html → overview_content.html} +30 -11
  56. django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
  57. django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
  58. django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
  59. django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
  60. django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
  61. django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
  62. django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
  63. django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
  64. django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
  65. django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
  66. django_cfg/apps/tasks/urls.py +2 -2
  67. django_cfg/apps/tasks/urls_admin.py +2 -2
  68. django_cfg/apps/tasks/utils/__init__.py +1 -0
  69. django_cfg/apps/tasks/utils/simulator.py +356 -0
  70. django_cfg/apps/tasks/views/__init__.py +16 -0
  71. django_cfg/apps/tasks/views/api.py +569 -0
  72. django_cfg/apps/tasks/views/dashboard.py +58 -0
  73. django_cfg/core/integration/__init__.py +21 -0
  74. django_cfg/management/commands/rundramatiq_simulator.py +430 -0
  75. django_cfg/models/constance.py +0 -11
  76. django_cfg/models/payments.py +137 -3
  77. django_cfg/modules/django_tasks.py +54 -21
  78. django_cfg/registry/core.py +4 -9
  79. django_cfg/template_archive/django_sample.zip +0 -0
  80. {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -2
  81. {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/RECORD +84 -152
  82. django_cfg/apps/payments/config/constance/__init__.py +0 -22
  83. django_cfg/apps/payments/config/constance/config_service.py +0 -123
  84. django_cfg/apps/payments/config/constance/fields.py +0 -69
  85. django_cfg/apps/payments/config/constance/settings.py +0 -160
  86. django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +0 -26
  87. django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +0 -28
  88. django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +0 -30
  89. django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
  90. django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
  91. django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
  92. django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
  93. django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
  94. django_cfg/apps/tasks/templates/tasks/base.html +0 -96
  95. django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
  96. django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
  97. django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
  98. django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
  99. django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
  100. django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
  101. django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
  102. django_cfg/apps/tasks/views.py +0 -461
  103. django_cfg/management/commands/app_agent_diagnose.py +0 -470
  104. django_cfg/management/commands/app_agent_generate.py +0 -342
  105. django_cfg/management/commands/app_agent_info.py +0 -308
  106. django_cfg/management/commands/auto_generate.py +0 -486
  107. django_cfg/modules/django_app_agent/__init__.py +0 -87
  108. django_cfg/modules/django_app_agent/agents/__init__.py +0 -40
  109. django_cfg/modules/django_app_agent/agents/base/__init__.py +0 -24
  110. django_cfg/modules/django_app_agent/agents/base/agent.py +0 -354
  111. django_cfg/modules/django_app_agent/agents/base/context.py +0 -236
  112. django_cfg/modules/django_app_agent/agents/base/executor.py +0 -430
  113. django_cfg/modules/django_app_agent/agents/generation/__init__.py +0 -12
  114. django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +0 -15
  115. django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +0 -147
  116. django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +0 -99
  117. django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +0 -32
  118. django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +0 -290
  119. django_cfg/modules/django_app_agent/agents/interfaces.py +0 -376
  120. django_cfg/modules/django_app_agent/core/__init__.py +0 -33
  121. django_cfg/modules/django_app_agent/core/config.py +0 -300
  122. django_cfg/modules/django_app_agent/core/exceptions.py +0 -359
  123. django_cfg/modules/django_app_agent/models/__init__.py +0 -71
  124. django_cfg/modules/django_app_agent/models/base.py +0 -283
  125. django_cfg/modules/django_app_agent/models/context.py +0 -496
  126. django_cfg/modules/django_app_agent/models/enums.py +0 -481
  127. django_cfg/modules/django_app_agent/models/requests.py +0 -500
  128. django_cfg/modules/django_app_agent/models/responses.py +0 -585
  129. django_cfg/modules/django_app_agent/pytest.ini +0 -6
  130. django_cfg/modules/django_app_agent/services/__init__.py +0 -42
  131. django_cfg/modules/django_app_agent/services/app_generator/__init__.py +0 -30
  132. django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +0 -133
  133. django_cfg/modules/django_app_agent/services/app_generator/context.py +0 -40
  134. django_cfg/modules/django_app_agent/services/app_generator/main.py +0 -202
  135. django_cfg/modules/django_app_agent/services/app_generator/structure.py +0 -316
  136. django_cfg/modules/django_app_agent/services/app_generator/validation.py +0 -125
  137. django_cfg/modules/django_app_agent/services/base.py +0 -437
  138. django_cfg/modules/django_app_agent/services/context_builder/__init__.py +0 -34
  139. django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +0 -141
  140. django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +0 -276
  141. django_cfg/modules/django_app_agent/services/context_builder/main.py +0 -272
  142. django_cfg/modules/django_app_agent/services/context_builder/models.py +0 -40
  143. django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +0 -85
  144. django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +0 -31
  145. django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +0 -311
  146. django_cfg/modules/django_app_agent/services/project_scanner/main.py +0 -221
  147. django_cfg/modules/django_app_agent/services/project_scanner/models.py +0 -59
  148. django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +0 -94
  149. django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +0 -28
  150. django_cfg/modules/django_app_agent/services/questioning_service/main.py +0 -273
  151. django_cfg/modules/django_app_agent/services/questioning_service/models.py +0 -111
  152. django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +0 -251
  153. django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +0 -347
  154. django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +0 -356
  155. django_cfg/modules/django_app_agent/services/report_service.py +0 -332
  156. django_cfg/modules/django_app_agent/services/template_manager/__init__.py +0 -18
  157. django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +0 -236
  158. django_cfg/modules/django_app_agent/services/template_manager/main.py +0 -159
  159. django_cfg/modules/django_app_agent/services/template_manager/models.py +0 -36
  160. django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +0 -100
  161. django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +0 -105
  162. django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +0 -31
  163. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +0 -44
  164. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +0 -81
  165. django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +0 -107
  166. django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +0 -139
  167. django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +0 -91
  168. django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +0 -195
  169. django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +0 -35
  170. django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +0 -211
  171. django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +0 -200
  172. django_cfg/modules/django_app_agent/services/validation_service/__init__.py +0 -25
  173. django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +0 -333
  174. django_cfg/modules/django_app_agent/services/validation_service/main.py +0 -242
  175. django_cfg/modules/django_app_agent/services/validation_service/models.py +0 -66
  176. django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +0 -352
  177. django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +0 -272
  178. django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +0 -203
  179. django_cfg/modules/django_app_agent/ui/__init__.py +0 -25
  180. django_cfg/modules/django_app_agent/ui/cli.py +0 -419
  181. django_cfg/modules/django_app_agent/ui/rich_components.py +0 -622
  182. django_cfg/modules/django_app_agent/utils/__init__.py +0 -38
  183. django_cfg/modules/django_app_agent/utils/logging.py +0 -360
  184. django_cfg/modules/django_app_agent/utils/validation.py +0 -417
  185. {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
  186. {django_cfg-1.3.9.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
  187. {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=request.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.crypto_amount = provider_response.amount
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.wallet_address = provider_response.wallet_address
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
- # Convert to PaymentData using our helper method
143
- payment_data = self._convert_payment_to_data(payment)
144
-
145
- self._log_operation(
146
- "create_payment",
147
- True,
148
- payment_id=str(payment.id),
149
- user_id=request.user_id,
150
- amount_usd=request.amount_usd
151
- )
152
-
153
- return PaymentResult(
154
- success=True,
155
- message="Payment created successfully",
156
- payment_id=str(payment.id),
157
- status=payment.status,
158
- amount_usd=payment.amount_usd,
159
- crypto_amount=payment.pay_amount,
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
- # This would integrate with provider services
368
- # For now, return success without changes
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
- {'status_changed': False}
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.sandbox
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
- response.raise_for_status()
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
- sandbox: bool = Field(default=False, description="Sandbox mode")
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.