django-cfg 1.3.7__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 (246) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +269 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +183 -460
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +153 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
  48. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
  49. django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
  50. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
  51. django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
  52. django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
  53. django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
  54. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +43 -17
  55. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  56. django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
  57. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +109 -63
  58. django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
  59. django_cfg/apps/payments/config/__init__.py +14 -15
  60. django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
  61. django_cfg/apps/payments/config/helpers.py +8 -13
  62. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  63. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  64. django_cfg/apps/payments/middleware/api_access.py +32 -6
  65. django_cfg/apps/payments/migrations/0001_initial.py +33 -46
  66. django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
  67. django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
  68. django_cfg/apps/payments/models/balance.py +12 -0
  69. django_cfg/apps/payments/models/currencies.py +106 -32
  70. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  71. django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
  72. django_cfg/apps/payments/models/payments.py +94 -0
  73. django_cfg/apps/payments/services/core/base.py +4 -4
  74. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  75. django_cfg/apps/payments/services/core/payment_service.py +266 -39
  76. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  77. django_cfg/apps/payments/services/providers/base.py +303 -41
  78. django_cfg/apps/payments/services/providers/models/__init__.py +42 -0
  79. django_cfg/apps/payments/services/providers/models/base.py +145 -0
  80. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  81. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  82. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  83. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  84. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  85. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  86. django_cfg/apps/payments/services/providers/nowpayments/provider.py +557 -0
  87. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  88. django_cfg/apps/payments/services/providers/registry.py +9 -37
  89. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  90. django_cfg/apps/payments/services/types/requests.py +19 -7
  91. django_cfg/apps/payments/signals/payment_signals.py +31 -2
  92. django_cfg/apps/payments/static/payments/js/api-client.js +29 -6
  93. django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
  94. django_cfg/apps/payments/static/payments/js/payment-form.js +98 -32
  95. django_cfg/apps/payments/tasks/__init__.py +39 -0
  96. django_cfg/apps/payments/tasks/types.py +73 -0
  97. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  98. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  99. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  100. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  101. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  102. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  103. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  104. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  105. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  106. django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
  107. django_cfg/apps/payments/urls.py +3 -2
  108. django_cfg/apps/payments/urls_admin.py +1 -1
  109. django_cfg/apps/payments/views/api/currencies.py +8 -5
  110. django_cfg/apps/payments/views/overview/services.py +2 -2
  111. django_cfg/apps/payments/views/serializers/currencies.py +22 -8
  112. django_cfg/apps/support/admin/__init__.py +10 -1
  113. django_cfg/apps/support/admin/support_admin.py +338 -141
  114. django_cfg/apps/tasks/admin/__init__.py +11 -0
  115. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  116. django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
  117. django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
  118. django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
  119. django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
  120. django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
  121. django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
  122. django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
  123. django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
  124. django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
  125. django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
  126. django_cfg/apps/tasks/tasks/__init__.py +10 -0
  127. django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
  128. django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
  129. django_cfg/apps/tasks/templates/tasks/components/{status_cards.html → overview_content.html} +30 -11
  130. django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
  131. django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
  132. django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
  133. django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
  134. django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
  135. django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
  136. django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
  137. django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
  138. django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
  139. django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
  140. django_cfg/apps/tasks/urls.py +2 -2
  141. django_cfg/apps/tasks/urls_admin.py +2 -2
  142. django_cfg/apps/tasks/utils/__init__.py +1 -0
  143. django_cfg/apps/tasks/utils/simulator.py +356 -0
  144. django_cfg/apps/tasks/views/__init__.py +16 -0
  145. django_cfg/apps/tasks/views/api.py +569 -0
  146. django_cfg/apps/tasks/views/dashboard.py +58 -0
  147. django_cfg/config.py +1 -1
  148. django_cfg/core/config.py +10 -5
  149. django_cfg/core/generation.py +1 -1
  150. django_cfg/core/integration/__init__.py +21 -0
  151. django_cfg/management/commands/__init__.py +13 -1
  152. django_cfg/management/commands/migrate_all.py +9 -3
  153. django_cfg/management/commands/migrator.py +11 -6
  154. django_cfg/management/commands/rundramatiq.py +3 -2
  155. django_cfg/management/commands/rundramatiq_simulator.py +430 -0
  156. django_cfg/middleware/__init__.py +0 -2
  157. django_cfg/models/api_keys.py +115 -0
  158. django_cfg/models/constance.py +0 -11
  159. django_cfg/models/payments.py +137 -3
  160. django_cfg/modules/django_admin/__init__.py +64 -0
  161. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  162. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  163. django_cfg/modules/django_admin/decorators/display.py +106 -0
  164. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  165. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  166. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  167. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  168. django_cfg/modules/django_admin/models/__init__.py +20 -0
  169. django_cfg/modules/django_admin/models/action_models.py +33 -0
  170. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  171. django_cfg/modules/django_admin/models/base.py +26 -0
  172. django_cfg/modules/django_admin/models/display_models.py +31 -0
  173. django_cfg/modules/django_admin/utils/badges.py +159 -0
  174. django_cfg/modules/django_admin/utils/displays.py +247 -0
  175. django_cfg/modules/django_currency/__init__.py +2 -2
  176. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  177. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  178. django_cfg/modules/django_currency/core/converter.py +12 -12
  179. django_cfg/modules/django_currency/database/__init__.py +2 -2
  180. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  181. django_cfg/modules/django_llm/llm/client.py +10 -2
  182. django_cfg/modules/django_tasks.py +54 -21
  183. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  184. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  185. django_cfg/modules/django_unfold/dashboard.py +14 -13
  186. django_cfg/modules/django_unfold/models/config.py +1 -1
  187. django_cfg/registry/core.py +7 -9
  188. django_cfg/registry/third_party.py +2 -2
  189. django_cfg/template_archive/django_sample.zip +0 -0
  190. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -1
  191. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/RECORD +198 -160
  192. django_cfg/apps/accounts/admin/activity.py +0 -96
  193. django_cfg/apps/accounts/admin/group.py +0 -17
  194. django_cfg/apps/accounts/admin/otp.py +0 -59
  195. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  196. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  197. django_cfg/apps/accounts/admin/user.py +0 -300
  198. django_cfg/apps/agents/core/agent.py +0 -281
  199. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  200. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  201. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  202. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  203. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  204. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  205. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  206. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  207. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  208. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  209. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  210. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  211. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  212. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  213. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  214. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  215. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  216. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  217. django_cfg/apps/payments/config/constance/__init__.py +0 -22
  218. django_cfg/apps/payments/config/constance/config_service.py +0 -123
  219. django_cfg/apps/payments/config/constance/fields.py +0 -69
  220. django_cfg/apps/payments/config/constance/settings.py +0 -160
  221. django_cfg/apps/payments/services/providers/nowpayments.py +0 -478
  222. django_cfg/apps/tasks/admin.py +0 -320
  223. django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
  224. django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
  225. django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
  226. django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
  227. django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
  228. django_cfg/apps/tasks/templates/tasks/base.html +0 -96
  229. django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
  230. django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
  231. django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
  232. django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
  233. django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
  234. django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
  235. django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
  236. django_cfg/apps/tasks/views.py +0 -461
  237. django_cfg/management/commands/auto_generate.py +0 -486
  238. django_cfg/middleware/static_nocache.py +0 -55
  239. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  240. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  241. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  242. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  243. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  244. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -16,6 +16,10 @@ from django.contrib.auth import get_user_model
16
16
  from ..models import APIKey, Subscription
17
17
  from ..config.helpers import MiddlewareConfigHelper
18
18
  from django_cfg.modules.django_logger import get_logger
19
+ from ..tasks.usage_tracking import (
20
+ update_api_key_usage_async,
21
+ update_subscription_usage_async
22
+ )
19
23
 
20
24
  User = get_user_model()
21
25
  logger = get_logger("api_access_middleware")
@@ -335,25 +339,47 @@ class APIAccessMiddleware(MiddlewareMixin):
335
339
 
336
340
  def _track_usage_async(self, api_key: APIKey, request: HttpRequest):
337
341
  """
338
- Track API usage asynchronously to avoid blocking the request.
342
+ Track API usage asynchronously using background tasks.
343
+
344
+ This method replaces the blocking database writes with async task queuing,
345
+ dramatically improving response times and reducing database load.
339
346
  """
340
347
  try:
341
- # Increment usage counter (this will trigger signals)
342
- api_key.increment_usage(ip_address=self._get_client_ip(request))
348
+ # Send API key usage update to background queue (non-blocking)
349
+ update_api_key_usage_async.send(
350
+ api_key_id=str(api_key.id),
351
+ ip_address=self._get_client_ip(request)
352
+ )
353
+
354
+ # If user has active subscription, update subscription usage
355
+ if hasattr(request, 'subscription_access') and request.subscription_access.get('allowed'):
356
+ subscription_id = request.subscription_access.get('subscription_id')
357
+ if subscription_id:
358
+ update_subscription_usage_async.send(
359
+ subscription_id=subscription_id
360
+ )
343
361
 
344
- # Update usage analytics in cache
362
+ # Update lightweight analytics in cache (fast operations only)
345
363
  today = timezone.now().date().isoformat()
346
364
 
347
- # Daily usage counter
365
+ # Daily usage counter (for quick dashboard access)
348
366
  daily_key = f"api_usage_daily:{api_key.user.id}:{today}"
349
367
  cache.set(daily_key, cache.get(daily_key, 0) + 1, timeout=86400 * 2)
350
368
 
351
- # Endpoint usage counter
369
+ # Endpoint usage counter (for analytics)
352
370
  endpoint_key = f"endpoint_usage:{request.path}:{today}"
353
371
  cache.set(endpoint_key, cache.get(endpoint_key, 0) + 1, timeout=86400 * 2)
354
372
 
373
+ logger.debug(f"Usage tracking queued", extra={
374
+ 'api_key_id': str(api_key.id),
375
+ 'user_id': api_key.user.id,
376
+ 'path': request.path,
377
+ 'method': 'background_tasks'
378
+ })
379
+
355
380
  except Exception as e:
356
381
  logger.warning(f"Usage tracking failed: {e}")
382
+ # Don't fail the request if usage tracking fails
357
383
 
358
384
  def _get_client_ip(self, request: HttpRequest) -> str:
359
385
  """
@@ -1,4 +1,4 @@
1
- # Generated by Django 5.2.6 on 2025-09-27 10:37
1
+ # Generated by Django 5.2.6 on 2025-09-29 06:18
2
2
 
3
3
  import django.core.validators
4
4
  import django.db.models.deletion
@@ -148,6 +148,18 @@ class Migration(migrations.Migration):
148
148
  max_length=50,
149
149
  ),
150
150
  ),
151
+ (
152
+ "usd_rate",
153
+ models.FloatField(
154
+ default=1.0, help_text="Current USD exchange rate (1 unit = X USD)"
155
+ ),
156
+ ),
157
+ (
158
+ "usd_rate_updated_at",
159
+ models.DateTimeField(
160
+ blank=True, help_text="When USD rate was last updated", null=True
161
+ ),
162
+ ),
151
163
  ],
152
164
  options={
153
165
  "verbose_name": "Currency",
@@ -272,50 +284,12 @@ class Migration(migrations.Migration):
272
284
  help_text="Currency code as used by the provider", max_length=20
273
285
  ),
274
286
  ),
275
- (
276
- "min_amount",
277
- models.DecimalField(
278
- blank=True,
279
- decimal_places=8,
280
- help_text="Minimum payment amount for this currency",
281
- max_digits=20,
282
- null=True,
283
- ),
284
- ),
285
- (
286
- "max_amount",
287
- models.DecimalField(
288
- blank=True,
289
- decimal_places=8,
290
- help_text="Maximum payment amount for this currency",
291
- max_digits=20,
292
- null=True,
293
- ),
294
- ),
295
287
  (
296
288
  "is_enabled",
297
289
  models.BooleanField(
298
290
  default=True, help_text="Whether this currency is enabled for this provider"
299
291
  ),
300
292
  ),
301
- (
302
- "fee_percentage",
303
- models.DecimalField(
304
- decimal_places=4,
305
- default=0,
306
- help_text="Fee percentage (0.0250 = 2.5%)",
307
- max_digits=5,
308
- ),
309
- ),
310
- (
311
- "fixed_fee",
312
- models.DecimalField(
313
- decimal_places=8,
314
- default=0,
315
- help_text="Fixed fee amount in this currency",
316
- max_digits=20,
317
- ),
318
- ),
319
293
  (
320
294
  "currency",
321
295
  models.ForeignKey(
@@ -981,7 +955,6 @@ class Migration(migrations.Migration):
981
955
  options={
982
956
  "verbose_name": "Universal Payment",
983
957
  "verbose_name_plural": "Universal Payments",
984
- "db_table": "payments_universal",
985
958
  "ordering": ["-created_at"],
986
959
  },
987
960
  ),
@@ -1002,6 +975,14 @@ class Migration(migrations.Migration):
1002
975
  validators=[django.core.validators.MinValueValidator(0.0)],
1003
976
  ),
1004
977
  ),
978
+ (
979
+ "reserved_usd",
980
+ models.FloatField(
981
+ default=0.0,
982
+ help_text="Reserved amount in USD (pending transactions)",
983
+ validators=[django.core.validators.MinValueValidator(0.0)],
984
+ ),
985
+ ),
1005
986
  (
1006
987
  "total_deposited",
1007
988
  models.FloatField(
@@ -1209,33 +1190,33 @@ class Migration(migrations.Migration):
1209
1190
  ),
1210
1191
  migrations.AddIndex(
1211
1192
  model_name="universalpayment",
1212
- index=models.Index(fields=["user", "status"], name="payments_un_user_id_8ce187_idx"),
1193
+ index=models.Index(fields=["user", "status"], name="payments_un_user_id_7f6e79_idx"),
1213
1194
  ),
1214
1195
  migrations.AddIndex(
1215
1196
  model_name="universalpayment",
1216
1197
  index=models.Index(
1217
- fields=["provider", "status"], name="payments_un_provide_904e1a_idx"
1198
+ fields=["provider", "status"], name="payments_un_provide_982d48_idx"
1218
1199
  ),
1219
1200
  ),
1220
1201
  migrations.AddIndex(
1221
1202
  model_name="universalpayment",
1222
1203
  index=models.Index(
1223
- fields=["status", "created_at"], name="payments_un_status_fd808c_idx"
1204
+ fields=["status", "created_at"], name="payments_un_status_eba1d1_idx"
1224
1205
  ),
1225
1206
  ),
1226
1207
  migrations.AddIndex(
1227
1208
  model_name="universalpayment",
1228
1209
  index=models.Index(
1229
- fields=["provider_payment_id"], name="payments_un_provide_553809_idx"
1210
+ fields=["provider_payment_id"], name="payments_un_provide_8ed72f_idx"
1230
1211
  ),
1231
1212
  ),
1232
1213
  migrations.AddIndex(
1233
1214
  model_name="universalpayment",
1234
- index=models.Index(fields=["transaction_hash"], name="payments_un_transac_5a0fe3_idx"),
1215
+ index=models.Index(fields=["transaction_hash"], name="payments_un_transac_6095d4_idx"),
1235
1216
  ),
1236
1217
  migrations.AddIndex(
1237
1218
  model_name="universalpayment",
1238
- index=models.Index(fields=["expires_at"], name="payments_un_expires_3b92ad_idx"),
1219
+ index=models.Index(fields=["expires_at"], name="payments_un_expires_6a9f9d_idx"),
1239
1220
  ),
1240
1221
  migrations.AddConstraint(
1241
1222
  model_name="universalpayment",
@@ -1265,4 +1246,10 @@ class Migration(migrations.Migration):
1265
1246
  condition=models.Q(("balance_usd__gte", 0.0)), name="balance_non_negative_check"
1266
1247
  ),
1267
1248
  ),
1249
+ migrations.AddConstraint(
1250
+ model_name="userbalance",
1251
+ constraint=models.CheckConstraint(
1252
+ condition=models.Q(("reserved_usd__gte", 0.0)), name="reserved_non_negative_check"
1253
+ ),
1254
+ ),
1268
1255
  ]
@@ -0,0 +1,46 @@
1
+ # Generated by Django 5.2.6 on 2025-09-29 06:23
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("payments", "0001_initial"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.RenameIndex(
13
+ model_name="universalpayment",
14
+ new_name="payments_un_user_id_8ce187_idx",
15
+ old_name="payments_un_user_id_7f6e79_idx",
16
+ ),
17
+ migrations.RenameIndex(
18
+ model_name="universalpayment",
19
+ new_name="payments_un_provide_904e1a_idx",
20
+ old_name="payments_un_provide_982d48_idx",
21
+ ),
22
+ migrations.RenameIndex(
23
+ model_name="universalpayment",
24
+ new_name="payments_un_status_fd808c_idx",
25
+ old_name="payments_un_status_eba1d1_idx",
26
+ ),
27
+ migrations.RenameIndex(
28
+ model_name="universalpayment",
29
+ new_name="payments_un_provide_553809_idx",
30
+ old_name="payments_un_provide_8ed72f_idx",
31
+ ),
32
+ migrations.RenameIndex(
33
+ model_name="universalpayment",
34
+ new_name="payments_un_transac_5a0fe3_idx",
35
+ old_name="payments_un_transac_6095d4_idx",
36
+ ),
37
+ migrations.RenameIndex(
38
+ model_name="universalpayment",
39
+ new_name="payments_un_expires_3b92ad_idx",
40
+ old_name="payments_un_expires_6a9f9d_idx",
41
+ ),
42
+ migrations.AlterModelTable(
43
+ name="universalpayment",
44
+ table="payments_universal",
45
+ ),
46
+ ]
@@ -0,0 +1,25 @@
1
+ # Generated by Django 5.2.6 on 2025-09-30 07:52
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ (
9
+ "payments",
10
+ "0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more",
11
+ ),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddField(
16
+ model_name="universalpayment",
17
+ name="status_changed_at",
18
+ field=models.DateTimeField(
19
+ blank=True,
20
+ db_index=True,
21
+ help_text="When the payment status was last changed",
22
+ null=True,
23
+ ),
24
+ ),
25
+ ]
@@ -35,6 +35,12 @@ class UserBalance(models.Model):
35
35
  help_text="Current balance in USD (float for performance)"
36
36
  )
37
37
 
38
+ reserved_usd = models.FloatField(
39
+ default=0.0,
40
+ validators=[MinValueValidator(0.0)],
41
+ help_text="Reserved amount in USD (pending transactions)"
42
+ )
43
+
38
44
  # Tracking fields
39
45
  total_deposited = models.FloatField(
40
46
  default=0.0,
@@ -75,6 +81,10 @@ class UserBalance(models.Model):
75
81
  check=models.Q(balance_usd__gte=0.0),
76
82
  name='balance_non_negative_check'
77
83
  ),
84
+ models.CheckConstraint(
85
+ check=models.Q(reserved_usd__gte=0.0),
86
+ name='reserved_non_negative_check'
87
+ ),
78
88
  ]
79
89
 
80
90
  def __str__(self):
@@ -84,6 +94,8 @@ class UserBalance(models.Model):
84
94
  """Validate balance data."""
85
95
  if self.balance_usd < 0:
86
96
  raise ValidationError("Balance cannot be negative")
97
+ if self.reserved_usd < 0:
98
+ raise ValidationError("Reserved amount cannot be negative")
87
99
 
88
100
  @property
89
101
  def balance_display(self) -> str:
@@ -62,6 +62,17 @@ class Currency(TimestampedModel):
62
62
  help_text="Source for exchange rates (auto-detected by django_currency)"
63
63
  )
64
64
 
65
+ usd_rate = models.FloatField(
66
+ default=1.0,
67
+ help_text="Current USD exchange rate (1 unit = X USD)"
68
+ )
69
+
70
+ usd_rate_updated_at = models.DateTimeField(
71
+ null=True,
72
+ blank=True,
73
+ help_text="When USD rate was last updated"
74
+ )
75
+
65
76
  # Manager
66
77
  from .managers.currency_managers import CurrencyManager
67
78
  objects = CurrencyManager()
@@ -100,6 +111,19 @@ class Currency(TimestampedModel):
100
111
  def is_fiat(self) -> bool:
101
112
  """Check if this is a fiat currency."""
102
113
  return self.currency_type == self.CurrencyType.FIAT
114
+
115
+ def get_provider_config(self, provider: str):
116
+ """
117
+ Get provider configuration for this currency.
118
+
119
+ Args:
120
+ provider: Provider name (e.g., 'nowpayments')
121
+
122
+ Returns:
123
+ dict: Provider configuration or None
124
+ """
125
+ return Currency.objects.get_provider_config(self.code, provider)
126
+
103
127
 
104
128
 
105
129
  class Network(TimestampedModel):
@@ -206,41 +230,14 @@ class ProviderCurrency(TimestampedModel):
206
230
  help_text="Currency code as used by the provider"
207
231
  )
208
232
 
209
- min_amount = models.DecimalField(
210
- max_digits=20,
211
- decimal_places=8,
212
- null=True,
213
- blank=True,
214
- help_text="Minimum payment amount for this currency"
215
- )
216
-
217
- max_amount = models.DecimalField(
218
- max_digits=20,
219
- decimal_places=8,
220
- null=True,
221
- blank=True,
222
- help_text="Maximum payment amount for this currency"
223
- )
233
+ # min_amount and max_amount removed - now provided by provider configuration properties
224
234
 
225
235
  is_enabled = models.BooleanField(
226
236
  default=True,
227
237
  help_text="Whether this currency is enabled for this provider"
228
238
  )
229
239
 
230
- # Fee configuration
231
- fee_percentage = models.DecimalField(
232
- max_digits=5,
233
- decimal_places=4,
234
- default=0,
235
- help_text="Fee percentage (0.0250 = 2.5%)"
236
- )
237
-
238
- fixed_fee = models.DecimalField(
239
- max_digits=20,
240
- decimal_places=8,
241
- default=0,
242
- help_text="Fixed fee amount in this currency"
243
- )
240
+ # Fee configuration removed - now provided by provider configuration properties
244
241
 
245
242
  class Meta:
246
243
  db_table = 'payments_provider_currencies'
@@ -267,12 +264,89 @@ class ProviderCurrency(TimestampedModel):
267
264
  if self.currency and self.currency.is_fiat and self.network:
268
265
  raise ValidationError("Fiat currencies should not specify a network")
269
266
 
270
- # Validate amount limits
271
- if self.min_amount and self.max_amount and self.min_amount > self.max_amount:
272
- raise ValidationError("Minimum amount cannot be greater than maximum amount")
267
+ # Amount limits validation removed - now handled by provider configuration
273
268
 
274
269
  @property
275
270
  def display_name(self) -> str:
276
271
  """Human-readable display name."""
277
272
  network_part = f" on {self.network.name}" if self.network else ""
278
273
  return f"{self.currency.name}{network_part}"
274
+
275
+ # Provider configuration properties (get values from provider config)
276
+ @property
277
+ def provider_fee_percentage(self) -> float:
278
+ """Get fee percentage from provider configuration."""
279
+ try:
280
+ from ..services.providers.registry import get_provider_registry
281
+ registry = get_provider_registry()
282
+ provider_instance = registry.get_provider(self.provider)
283
+ if provider_instance:
284
+ return float(provider_instance.get_fee_percentage(self.currency.code, self.currency.currency_type))
285
+ return 0.005 # Default 0.5%
286
+ except Exception:
287
+ return 0.005 # Default fallback
288
+
289
+ @property
290
+ def provider_fixed_fee_usd(self) -> float:
291
+ """Get fixed fee from provider configuration."""
292
+ try:
293
+ from ..services.providers.registry import get_provider_registry
294
+ registry = get_provider_registry()
295
+ provider_instance = registry.get_provider(self.provider)
296
+ if provider_instance:
297
+ return float(provider_instance.get_fixed_fee_usd(self.currency.code, self.currency.currency_type))
298
+ return 0.0 # Default no fee
299
+ except Exception:
300
+ return 0.0 # Default fallback
301
+
302
+ @property
303
+ def provider_min_amount_usd(self) -> float:
304
+ """Get minimum amount from provider configuration."""
305
+ try:
306
+ from ..services.providers.registry import get_provider_registry
307
+ registry = get_provider_registry()
308
+ provider_instance = registry.get_provider(self.provider)
309
+ if provider_instance:
310
+ return float(provider_instance.get_min_amount_usd(self.currency.code, self.currency.currency_type))
311
+ return 0.000001 # Default minimum
312
+ except Exception:
313
+ return 0.000001 # Default fallback
314
+
315
+ @property
316
+ def provider_max_amount_usd(self) -> float:
317
+ """Get maximum amount from provider configuration."""
318
+ try:
319
+ from ..services.providers.registry import get_provider_registry
320
+ registry = get_provider_registry()
321
+ provider_instance = registry.get_provider(self.provider)
322
+ if provider_instance:
323
+ return float(provider_instance.get_max_amount_usd(self.currency.code, self.currency.currency_type))
324
+ return 1000000.0 # Default maximum
325
+ except Exception:
326
+ return 1000000.0 # Default fallback
327
+
328
+ @property
329
+ def provider_confirmation_blocks(self) -> int:
330
+ """Get confirmation blocks from provider configuration."""
331
+ try:
332
+ from ..services.providers.registry import get_provider_registry
333
+ registry = get_provider_registry()
334
+ provider_instance = registry.get_provider(self.provider)
335
+ if provider_instance and self.network:
336
+ return provider_instance.get_confirmation_blocks(self.network.code)
337
+ return 1 # Default
338
+ except Exception:
339
+ return 1 # Default fallback
340
+
341
+ @property
342
+ def provider_network_name(self) -> str:
343
+ """Get network name from provider configuration."""
344
+ try:
345
+ from ..services.providers.registry import get_provider_registry
346
+ registry = get_provider_registry()
347
+ provider_instance = registry.get_provider(self.provider)
348
+ if provider_instance and self.network:
349
+ return provider_instance.get_network_name(self.network.code)
350
+ return self.network.name if self.network else 'Unknown'
351
+ except Exception:
352
+ return self.network.name if self.network else 'Unknown'
@@ -383,3 +383,68 @@ class CurrencyManager(models.Manager):
383
383
  stats['by_provider'][provider] = queryset.supported_by_provider(provider).count()
384
384
 
385
385
  return stats
386
+
387
+ def get_provider_config(self, currency_code: str, provider: str):
388
+ """
389
+ Get provider configuration for currency.
390
+
391
+ Args:
392
+ currency_code: Currency code (e.g., 'BTC')
393
+ provider: Provider name (e.g., 'nowpayments')
394
+
395
+ Returns:
396
+ dict: Provider configuration or None
397
+ """
398
+ try:
399
+ from ..currencies import ProviderCurrency
400
+ from ...services.providers.registry import get_provider_registry
401
+
402
+ # Get provider instance
403
+ registry = get_provider_registry()
404
+ provider_instance = registry.get_provider(provider)
405
+
406
+ if not provider_instance:
407
+ return None
408
+
409
+ # Get currency
410
+ currency = self.by_code(currency_code)
411
+ if not currency:
412
+ return None
413
+
414
+ # Get provider currency config from DB
415
+ try:
416
+ provider_currency = ProviderCurrency.objects.get(
417
+ currency=currency,
418
+ provider=provider,
419
+ is_enabled=True
420
+ )
421
+
422
+ # Get configuration from provider
423
+ return {
424
+ 'fee_percentage': float(provider_instance.get_fee_percentage(currency_code, currency.currency_type)),
425
+ 'fixed_fee_usd': float(provider_instance.get_fixed_fee_usd(currency_code, currency.currency_type)),
426
+ 'min_amount_usd': float(provider_instance.get_min_amount_usd(currency_code, currency.currency_type)),
427
+ 'max_amount_usd': float(provider_instance.get_max_amount_usd(currency_code, currency.currency_type)),
428
+ 'confirmation_blocks': provider_instance.get_confirmation_blocks(provider_currency.network.code if provider_currency.network else ''),
429
+ 'network_name': provider_instance.get_network_name(provider_currency.network.code if provider_currency.network else ''),
430
+ 'network_code': provider_currency.network.code if provider_currency.network else None,
431
+ }
432
+
433
+ except ProviderCurrency.DoesNotExist:
434
+ # Return default configuration from provider
435
+ return {
436
+ 'fee_percentage': float(provider_instance.get_fee_percentage(currency_code, currency.currency_type)),
437
+ 'fixed_fee_usd': float(provider_instance.get_fixed_fee_usd(currency_code, currency.currency_type)),
438
+ 'min_amount_usd': float(provider_instance.get_min_amount_usd(currency_code, currency.currency_type)),
439
+ 'max_amount_usd': float(provider_instance.get_max_amount_usd(currency_code, currency.currency_type)),
440
+ 'confirmation_blocks': 1,
441
+ 'network_name': 'Unknown',
442
+ 'network_code': None,
443
+ }
444
+
445
+ except Exception as e:
446
+ logger.error(f"Failed to get provider config: {e}", extra={
447
+ 'currency_code': currency_code,
448
+ 'provider': provider
449
+ })
450
+ return None