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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/admin/__init__.py +24 -8
- django_cfg/apps/accounts/admin/activity_admin.py +146 -0
- django_cfg/apps/accounts/admin/filters.py +98 -22
- django_cfg/apps/accounts/admin/group_admin.py +86 -0
- django_cfg/apps/accounts/admin/inlines.py +42 -13
- django_cfg/apps/accounts/admin/otp_admin.py +115 -0
- django_cfg/apps/accounts/admin/registration_admin.py +173 -0
- django_cfg/apps/accounts/admin/resources.py +123 -19
- django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
- django_cfg/apps/accounts/admin/user_admin.py +362 -0
- django_cfg/apps/agents/admin/__init__.py +17 -4
- django_cfg/apps/agents/admin/execution_admin.py +204 -183
- django_cfg/apps/agents/admin/registry_admin.py +230 -255
- django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
- django_cfg/apps/agents/core/__init__.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +221 -0
- django_cfg/apps/agents/core/exceptions.py +14 -0
- django_cfg/apps/agents/core/orchestrator.py +18 -3
- django_cfg/apps/knowbase/admin/__init__.py +1 -1
- django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
- django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
- django_cfg/apps/knowbase/admin/document_admin.py +269 -262
- django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
- django_cfg/apps/knowbase/config/settings.py +21 -4
- django_cfg/apps/knowbase/views/chat_views.py +3 -0
- django_cfg/apps/leads/admin/__init__.py +3 -1
- django_cfg/apps/leads/admin/leads_admin.py +235 -35
- django_cfg/apps/maintenance/admin/__init__.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
- django_cfg/apps/maintenance/admin/log_admin.py +143 -61
- django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
- django_cfg/apps/maintenance/admin/site_admin.py +213 -352
- django_cfg/apps/newsletter/admin/__init__.py +29 -2
- django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
- django_cfg/apps/payments/admin/__init__.py +18 -27
- django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
- django_cfg/apps/payments/admin/balance_admin.py +166 -632
- django_cfg/apps/payments/admin/currencies_admin.py +235 -607
- django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
- django_cfg/apps/payments/admin/filters.py +83 -3
- django_cfg/apps/payments/admin/networks_admin.py +269 -0
- django_cfg/apps/payments/admin/payments_admin.py +183 -460
- django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
- django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +153 -34
- django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +43 -17
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +109 -63
- django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
- django_cfg/apps/payments/config/__init__.py +14 -15
- django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
- django_cfg/apps/payments/config/helpers.py +8 -13
- django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
- django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
- django_cfg/apps/payments/middleware/api_access.py +32 -6
- django_cfg/apps/payments/migrations/0001_initial.py +33 -46
- django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
- django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
- django_cfg/apps/payments/models/balance.py +12 -0
- django_cfg/apps/payments/models/currencies.py +106 -32
- django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
- django_cfg/apps/payments/models/payments.py +94 -0
- django_cfg/apps/payments/services/core/base.py +4 -4
- django_cfg/apps/payments/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +266 -39
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +303 -41
- django_cfg/apps/payments/services/providers/models/__init__.py +42 -0
- django_cfg/apps/payments/services/providers/models/base.py +145 -0
- django_cfg/apps/payments/services/providers/models/providers.py +87 -0
- django_cfg/apps/payments/services/providers/models/universal.py +48 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
- django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +557 -0
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +9 -37
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/services/types/requests.py +19 -7
- django_cfg/apps/payments/signals/payment_signals.py +31 -2
- django_cfg/apps/payments/static/payments/js/api-client.js +29 -6
- django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
- django_cfg/apps/payments/static/payments/js/payment-form.js +98 -32
- django_cfg/apps/payments/tasks/__init__.py +39 -0
- django_cfg/apps/payments/tasks/types.py +73 -0
- django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
- django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
- django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
- django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
- django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
- django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
- django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
- django_cfg/apps/payments/urls.py +3 -2
- django_cfg/apps/payments/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +8 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +22 -8
- django_cfg/apps/support/admin/__init__.py +10 -1
- django_cfg/apps/support/admin/support_admin.py +338 -141
- django_cfg/apps/tasks/admin/__init__.py +11 -0
- django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
- django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
- django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
- django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
- django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
- django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
- django_cfg/apps/tasks/tasks/__init__.py +10 -0
- django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
- django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
- django_cfg/apps/tasks/templates/tasks/components/{status_cards.html → overview_content.html} +30 -11
- django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
- django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
- django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
- django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
- django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
- django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
- django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
- django_cfg/apps/tasks/urls.py +2 -2
- django_cfg/apps/tasks/urls_admin.py +2 -2
- django_cfg/apps/tasks/utils/__init__.py +1 -0
- django_cfg/apps/tasks/utils/simulator.py +356 -0
- django_cfg/apps/tasks/views/__init__.py +16 -0
- django_cfg/apps/tasks/views/api.py +569 -0
- django_cfg/apps/tasks/views/dashboard.py +58 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/core/integration/__init__.py +21 -0
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/migrate_all.py +9 -3
- django_cfg/management/commands/migrator.py +11 -6
- django_cfg/management/commands/rundramatiq.py +3 -2
- django_cfg/management/commands/rundramatiq_simulator.py +430 -0
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- django_cfg/models/constance.py +0 -11
- django_cfg/models/payments.py +137 -3
- django_cfg/modules/django_admin/__init__.py +64 -0
- django_cfg/modules/django_admin/decorators/__init__.py +13 -0
- django_cfg/modules/django_admin/decorators/actions.py +106 -0
- django_cfg/modules/django_admin/decorators/display.py +106 -0
- django_cfg/modules/django_admin/mixins/__init__.py +14 -0
- django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
- django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
- django_cfg/modules/django_admin/models/__init__.py +20 -0
- django_cfg/modules/django_admin/models/action_models.py +33 -0
- django_cfg/modules/django_admin/models/badge_models.py +20 -0
- django_cfg/modules/django_admin/models/base.py +26 -0
- django_cfg/modules/django_admin/models/display_models.py +31 -0
- django_cfg/modules/django_admin/utils/badges.py +159 -0
- django_cfg/modules/django_admin/utils/displays.py +247 -0
- django_cfg/modules/django_currency/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
- django_cfg/modules/django_currency/core/converter.py +12 -12
- django_cfg/modules/django_currency/database/__init__.py +2 -2
- django_cfg/modules/django_currency/database/database_loader.py +93 -42
- django_cfg/modules/django_llm/llm/client.py +10 -2
- django_cfg/modules/django_tasks.py +54 -21
- django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
- django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
- django_cfg/modules/django_unfold/dashboard.py +14 -13
- django_cfg/modules/django_unfold/models/config.py +1 -1
- django_cfg/registry/core.py +7 -9
- django_cfg/registry/third_party.py +2 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -1
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/RECORD +198 -160
- django_cfg/apps/accounts/admin/activity.py +0 -96
- django_cfg/apps/accounts/admin/group.py +0 -17
- django_cfg/apps/accounts/admin/otp.py +0 -59
- django_cfg/apps/accounts/admin/registration_source.py +0 -97
- django_cfg/apps/accounts/admin/twilio_response.py +0 -227
- django_cfg/apps/accounts/admin/user.py +0 -300
- django_cfg/apps/agents/core/agent.py +0 -281
- django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
- django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
- django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
- django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
- django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
- django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
- django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
- django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
- django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
- django_cfg/apps/payments/config/constance/__init__.py +0 -22
- django_cfg/apps/payments/config/constance/config_service.py +0 -123
- django_cfg/apps/payments/config/constance/fields.py +0 -69
- django_cfg/apps/payments/config/constance/settings.py +0 -160
- django_cfg/apps/payments/services/providers/nowpayments.py +0 -478
- django_cfg/apps/tasks/admin.py +0 -320
- django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
- django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
- django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
- django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
- django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
- django_cfg/apps/tasks/templates/tasks/base.html +0 -96
- django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
- django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
- django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
- django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
- django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
- django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
- django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
- django_cfg/apps/tasks/views.py +0 -461
- django_cfg/management/commands/auto_generate.py +0 -486
- django_cfg/middleware/static_nocache.py +0 -55
- django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
- /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
-
#
|
342
|
-
|
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
|
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-
|
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="
|
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="
|
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="
|
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="
|
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="
|
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="
|
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
|
-
|
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
|
-
#
|
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
|