django-cfg 1.3.7__py3-none-any.whl → 1.3.9__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 +258 -0
- django_cfg/apps/payments/admin/payments_admin.py +171 -461
- 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 +105 -34
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
- 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/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
- django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
- django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -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/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +1 -1
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +95 -39
- django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
- django_cfg/apps/payments/services/providers/models/base.py +122 -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.py → nowpayments/provider.py} +240 -209
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -32
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
- django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
- 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/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +5 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +4 -3
- 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/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/app_agent_diagnose.py +470 -0
- django_cfg/management/commands/app_agent_generate.py +342 -0
- django_cfg/management/commands/app_agent_info.py +308 -0
- 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/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- 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_app_agent/__init__.py +87 -0
- django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
- django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
- django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
- django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
- django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
- django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
- django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
- django_cfg/modules/django_app_agent/core/__init__.py +33 -0
- django_cfg/modules/django_app_agent/core/config.py +300 -0
- django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
- django_cfg/modules/django_app_agent/models/__init__.py +71 -0
- django_cfg/modules/django_app_agent/models/base.py +283 -0
- django_cfg/modules/django_app_agent/models/context.py +496 -0
- django_cfg/modules/django_app_agent/models/enums.py +481 -0
- django_cfg/modules/django_app_agent/models/requests.py +500 -0
- django_cfg/modules/django_app_agent/models/responses.py +585 -0
- django_cfg/modules/django_app_agent/pytest.ini +6 -0
- django_cfg/modules/django_app_agent/services/__init__.py +42 -0
- django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
- django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
- django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
- django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
- django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
- django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
- django_cfg/modules/django_app_agent/services/base.py +437 -0
- django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
- django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
- django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
- django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
- django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
- django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
- django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
- django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
- django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
- django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
- django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
- django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
- django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
- django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
- django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
- django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
- django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
- django_cfg/modules/django_app_agent/services/report_service.py +332 -0
- django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
- django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
- django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
- django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
- django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
- django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
- django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
- django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
- django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
- django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
- django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
- django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
- django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
- django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
- django_cfg/modules/django_app_agent/ui/cli.py +419 -0
- django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
- django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
- django_cfg/modules/django_app_agent/utils/logging.py +360 -0
- django_cfg/modules/django_app_agent/utils/validation.py +417 -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_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 +3 -0
- 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.9.dist-info}/METADATA +2 -1
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/RECORD +223 -117
- 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/tasks/admin.py +0 -320
- 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.9.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.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
|
"""
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Generated by Django 5.2.6 on 2025-09-27 12:59
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
dependencies = [
|
8
|
+
("payments", "0001_initial"),
|
9
|
+
]
|
10
|
+
|
11
|
+
operations = [
|
12
|
+
migrations.AddField(
|
13
|
+
model_name="currency",
|
14
|
+
name="usd_rate",
|
15
|
+
field=models.FloatField(
|
16
|
+
default=1.0, help_text="Current USD exchange rate (1 unit = X USD)"
|
17
|
+
),
|
18
|
+
),
|
19
|
+
migrations.AddField(
|
20
|
+
model_name="currency",
|
21
|
+
name="usd_rate_updated_at",
|
22
|
+
field=models.DateTimeField(
|
23
|
+
blank=True, help_text="When USD rate was last updated", null=True
|
24
|
+
),
|
25
|
+
),
|
26
|
+
]
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Generated by Django 5.2.6 on 2025-09-27 16:46
|
2
|
+
|
3
|
+
from django.db import migrations
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
dependencies = [
|
8
|
+
("payments", "0002_currency_usd_rate_currency_usd_rate_updated_at"),
|
9
|
+
]
|
10
|
+
|
11
|
+
operations = [
|
12
|
+
migrations.RemoveField(
|
13
|
+
model_name="providercurrency",
|
14
|
+
name="fee_percentage",
|
15
|
+
),
|
16
|
+
migrations.RemoveField(
|
17
|
+
model_name="providercurrency",
|
18
|
+
name="fixed_fee",
|
19
|
+
),
|
20
|
+
migrations.RemoveField(
|
21
|
+
model_name="providercurrency",
|
22
|
+
name="max_amount",
|
23
|
+
),
|
24
|
+
migrations.RemoveField(
|
25
|
+
model_name="providercurrency",
|
26
|
+
name="min_amount",
|
27
|
+
),
|
28
|
+
]
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Generated by Django 5.2.6 on 2025-09-27 18:16
|
2
|
+
|
3
|
+
import django.core.validators
|
4
|
+
from django.conf import settings
|
5
|
+
from django.db import migrations, models
|
6
|
+
|
7
|
+
|
8
|
+
class Migration(migrations.Migration):
|
9
|
+
dependencies = [
|
10
|
+
("payments", "0003_remove_provider_currency_fields"),
|
11
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
12
|
+
]
|
13
|
+
|
14
|
+
operations = [
|
15
|
+
migrations.AddField(
|
16
|
+
model_name="userbalance",
|
17
|
+
name="reserved_usd",
|
18
|
+
field=models.FloatField(
|
19
|
+
default=0.0,
|
20
|
+
help_text="Reserved amount in USD (pending transactions)",
|
21
|
+
validators=[django.core.validators.MinValueValidator(0.0)],
|
22
|
+
),
|
23
|
+
),
|
24
|
+
migrations.AddConstraint(
|
25
|
+
model_name="userbalance",
|
26
|
+
constraint=models.CheckConstraint(
|
27
|
+
condition=models.Q(("reserved_usd__gte", 0.0)), name="reserved_non_negative_check"
|
28
|
+
),
|
29
|
+
),
|
30
|
+
]
|
@@ -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
|
@@ -4,10 +4,9 @@ Currency service for the Universal Payment System v2.0.
|
|
4
4
|
Handles currency conversion and rate management using django_currency module.
|
5
5
|
"""
|
6
6
|
|
7
|
-
from typing import Optional,
|
8
|
-
from decimal import Decimal
|
7
|
+
from typing import Optional, List
|
9
8
|
from django.utils import timezone
|
10
|
-
from
|
9
|
+
from django.db import models
|
11
10
|
|
12
11
|
from .base import BaseService
|
13
12
|
from ..types import (
|
@@ -161,29 +160,35 @@ class CurrencyService(BaseService):
|
|
161
160
|
quote_currency=quote_currency
|
162
161
|
)
|
163
162
|
|
164
|
-
def get_supported_currencies(self, provider: Optional[str] = None) -> ServiceOperationResult:
|
163
|
+
def get_supported_currencies(self, provider: Optional[str] = None, currency_type: str = 'all') -> ServiceOperationResult:
|
165
164
|
"""
|
166
165
|
Get list of supported currencies.
|
167
166
|
|
168
167
|
Args:
|
169
168
|
provider: Filter by provider (optional)
|
169
|
+
currency_type: Filter by currency type ('all', 'crypto', 'fiat')
|
170
170
|
|
171
171
|
Returns:
|
172
172
|
ServiceOperationResult: List of supported currencies
|
173
173
|
"""
|
174
174
|
try:
|
175
175
|
self.logger.debug("Getting supported currencies", extra={
|
176
|
-
'provider': provider
|
176
|
+
'provider': provider,
|
177
|
+
'currency_type': currency_type
|
177
178
|
})
|
178
179
|
|
179
180
|
# Get currencies from database
|
180
|
-
queryset = Currency.objects.filter(
|
181
|
+
queryset = Currency.objects.filter(is_active=True)
|
182
|
+
|
183
|
+
# Filter by currency type
|
184
|
+
if currency_type and currency_type != 'all':
|
185
|
+
queryset = queryset.filter(currency_type=currency_type)
|
181
186
|
|
182
187
|
if provider:
|
183
188
|
# Filter by provider support
|
184
189
|
queryset = queryset.filter(
|
185
|
-
|
186
|
-
|
190
|
+
provider_configs__provider=provider,
|
191
|
+
provider_configs__is_enabled=True
|
187
192
|
).distinct()
|
188
193
|
|
189
194
|
currencies = queryset.order_by('code')
|
@@ -197,18 +202,20 @@ class CurrencyService(BaseService):
|
|
197
202
|
# Add provider-specific info if requested
|
198
203
|
if provider:
|
199
204
|
try:
|
200
|
-
provider_currency = ProviderCurrency.objects.
|
205
|
+
provider_currency = ProviderCurrency.objects.filter(
|
201
206
|
currency=currency,
|
202
207
|
provider=provider,
|
203
208
|
is_enabled=True
|
204
|
-
)
|
205
|
-
|
206
|
-
'
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
209
|
+
).first()
|
210
|
+
if provider_currency:
|
211
|
+
currency_info['provider_info'] = {
|
212
|
+
'min_amount': provider_currency.provider_min_amount_usd,
|
213
|
+
'max_amount': provider_currency.provider_max_amount_usd,
|
214
|
+
'fee_percentage': provider_currency.provider_fee_percentage,
|
215
|
+
'fixed_fee_usd': provider_currency.provider_fixed_fee_usd,
|
216
|
+
'confirmation_blocks': provider_currency.provider_confirmation_blocks
|
217
|
+
}
|
218
|
+
except Exception:
|
212
219
|
pass
|
213
220
|
|
214
221
|
currency_data.append(currency_info)
|
@@ -245,7 +252,7 @@ class CurrencyService(BaseService):
|
|
245
252
|
|
246
253
|
# Get currency
|
247
254
|
try:
|
248
|
-
currency = Currency.objects.get(code=currency_code,
|
255
|
+
currency = Currency.objects.get(code=currency_code, is_active=True)
|
249
256
|
except Currency.DoesNotExist:
|
250
257
|
return self._create_error_result(
|
251
258
|
f"Currency {currency_code} not found or disabled",
|
@@ -255,7 +262,7 @@ class CurrencyService(BaseService):
|
|
255
262
|
# Get networks
|
256
263
|
networks = Network.objects.filter(
|
257
264
|
currency_code=currency_code,
|
258
|
-
|
265
|
+
is_active=True
|
259
266
|
).order_by('name')
|
260
267
|
|
261
268
|
network_data = []
|
@@ -305,10 +312,10 @@ class CurrencyService(BaseService):
|
|
305
312
|
if currency_codes:
|
306
313
|
currencies = Currency.objects.filter(
|
307
314
|
code__in=currency_codes,
|
308
|
-
|
315
|
+
is_active=True
|
309
316
|
)
|
310
317
|
else:
|
311
|
-
currencies = Currency.objects.filter(
|
318
|
+
currencies = Currency.objects.filter(is_active=True)
|
312
319
|
|
313
320
|
updated_count = 0
|
314
321
|
failed_count = 0
|
@@ -369,14 +376,14 @@ class CurrencyService(BaseService):
|
|
369
376
|
try:
|
370
377
|
# Currency counts
|
371
378
|
total_currencies = Currency.objects.count()
|
372
|
-
enabled_currencies = Currency.objects.filter(
|
379
|
+
enabled_currencies = Currency.objects.filter(is_active=True).count()
|
373
380
|
crypto_currencies = Currency.objects.filter(
|
374
381
|
currency_type=Currency.CurrencyType.CRYPTO,
|
375
|
-
|
382
|
+
is_active=True
|
376
383
|
).count()
|
377
384
|
fiat_currencies = Currency.objects.filter(
|
378
385
|
currency_type=Currency.CurrencyType.FIAT,
|
379
|
-
|
386
|
+
is_active=True
|
380
387
|
).count()
|
381
388
|
|
382
389
|
# Provider support
|
@@ -388,7 +395,7 @@ class CurrencyService(BaseService):
|
|
388
395
|
|
389
396
|
# Network stats
|
390
397
|
network_stats = Network.objects.filter(
|
391
|
-
|
398
|
+
is_active=True
|
392
399
|
).values('currency_code').annotate(
|
393
400
|
network_count=models.Count('id')
|
394
401
|
).order_by('-network_count')
|
@@ -415,14 +422,14 @@ class CurrencyService(BaseService):
|
|
415
422
|
"""Validate that currencies are supported."""
|
416
423
|
try:
|
417
424
|
# Check from_currency
|
418
|
-
if not Currency.objects.filter(code=from_currency,
|
425
|
+
if not Currency.objects.filter(code=from_currency, is_active=True).exists():
|
419
426
|
return self._create_error_result(
|
420
427
|
f"Currency {from_currency} not supported",
|
421
428
|
"from_currency_not_supported"
|
422
429
|
)
|
423
430
|
|
424
431
|
# Check to_currency
|
425
|
-
if not Currency.objects.filter(code=to_currency,
|
432
|
+
if not Currency.objects.filter(code=to_currency, is_active=True).exists():
|
426
433
|
return self._create_error_result(
|
427
434
|
f"Currency {to_currency} not supported",
|
428
435
|
"to_currency_not_supported"
|
@@ -440,7 +447,7 @@ class CurrencyService(BaseService):
|
|
440
447
|
"""Perform currency service health check."""
|
441
448
|
try:
|
442
449
|
# Check database connectivity
|
443
|
-
currency_count = Currency.objects.filter(
|
450
|
+
currency_count = Currency.objects.filter(is_active=True).count()
|
444
451
|
|
445
452
|
# Test currency conversion
|
446
453
|
try:
|
@@ -104,7 +104,7 @@ class PaymentService(BaseService):
|
|
104
104
|
payment = self._execute_with_transaction(create_payment_transaction)
|
105
105
|
|
106
106
|
# Create payment with provider
|
107
|
-
from ..providers.
|
107
|
+
from ..providers.models import PaymentRequest as ProviderPaymentRequest
|
108
108
|
|
109
109
|
provider_request = ProviderPaymentRequest(
|
110
110
|
amount_usd=request.amount_usd,
|
@@ -7,6 +7,7 @@ Provider implementations with unified interface and Pydantic validation.
|
|
7
7
|
from .base import BaseProvider
|
8
8
|
from .nowpayments import NowPaymentsProvider
|
9
9
|
from .registry import ProviderRegistry, get_provider_registry, initialize_providers
|
10
|
+
from .sync_service import ProviderSyncService, get_provider_sync_service
|
10
11
|
|
11
12
|
__all__ = [
|
12
13
|
'BaseProvider',
|
@@ -14,4 +15,6 @@ __all__ = [
|
|
14
15
|
'ProviderRegistry',
|
15
16
|
'get_provider_registry',
|
16
17
|
'initialize_providers',
|
18
|
+
'ProviderSyncService',
|
19
|
+
'get_provider_sync_service',
|
17
20
|
]
|