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