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