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
@@ -0,0 +1,196 @@
|
|
1
|
+
"""
|
2
|
+
NowPayments currency synchronization service.
|
3
|
+
|
4
|
+
Handles syncing currencies from NowPayments API to database.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Optional, Tuple, List
|
8
|
+
from django.db import transaction
|
9
|
+
from django.utils import timezone
|
10
|
+
|
11
|
+
from django_cfg.modules.django_logger import get_logger
|
12
|
+
from django_cfg.apps.payments.models import Currency, Network, ProviderCurrency
|
13
|
+
from ..models import UniversalCurrency, CurrencySyncResult
|
14
|
+
from .config import NowPaymentsConfig as Config
|
15
|
+
|
16
|
+
logger = get_logger("nowpayments_sync")
|
17
|
+
|
18
|
+
|
19
|
+
class NowPaymentsCurrencySync:
|
20
|
+
"""Service for synchronizing NowPayments currencies to database."""
|
21
|
+
|
22
|
+
def __init__(self, provider_name: str = 'nowpayments'):
|
23
|
+
"""Initialize currency sync service."""
|
24
|
+
self.provider_name = provider_name
|
25
|
+
|
26
|
+
def sync_currencies_to_db(self, currencies: List[UniversalCurrency]) -> CurrencySyncResult:
|
27
|
+
"""
|
28
|
+
Sync universal currencies to database.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
currencies: List of universal currencies from provider
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
CurrencySyncResult: Sync operation results
|
35
|
+
"""
|
36
|
+
result = CurrencySyncResult(total_processed=len(currencies))
|
37
|
+
|
38
|
+
try:
|
39
|
+
with transaction.atomic():
|
40
|
+
for currency in currencies:
|
41
|
+
try:
|
42
|
+
self._sync_single_currency(currency, result)
|
43
|
+
except Exception as e:
|
44
|
+
error_msg = f"Failed to sync {currency.provider_currency_code}: {e}"
|
45
|
+
logger.error(error_msg)
|
46
|
+
result.errors.append(error_msg)
|
47
|
+
|
48
|
+
logger.info(
|
49
|
+
f"Currency sync completed: {result.currencies_created} created, "
|
50
|
+
f"{result.currencies_updated} updated, "
|
51
|
+
f"{result.provider_currencies_created} provider currencies created, "
|
52
|
+
f"{len(result.errors)} errors"
|
53
|
+
)
|
54
|
+
|
55
|
+
return result
|
56
|
+
|
57
|
+
except Exception as e:
|
58
|
+
logger.error(f"Currency sync transaction failed: {e}")
|
59
|
+
result.errors.append(f"Transaction failed: {e}")
|
60
|
+
return result
|
61
|
+
|
62
|
+
def _sync_single_currency(self, currency: UniversalCurrency, result: CurrencySyncResult):
|
63
|
+
"""Sync a single currency to database."""
|
64
|
+
|
65
|
+
# 1. Ensure base currency exists
|
66
|
+
base_currency, currency_created = self._get_or_create_currency(currency)
|
67
|
+
if currency_created:
|
68
|
+
result.currencies_created += 1
|
69
|
+
else:
|
70
|
+
# Update existing currency if needed
|
71
|
+
updated = self._update_currency_if_needed(base_currency, currency)
|
72
|
+
if updated:
|
73
|
+
result.currencies_updated += 1
|
74
|
+
|
75
|
+
# 2. Ensure network exists (if applicable)
|
76
|
+
network = None
|
77
|
+
if currency.network_code:
|
78
|
+
network, network_created = self._get_or_create_network(currency, base_currency)
|
79
|
+
if network_created:
|
80
|
+
result.networks_created += 1
|
81
|
+
|
82
|
+
# 3. Create or update provider currency
|
83
|
+
provider_currency, pc_created = self._get_or_create_provider_currency(
|
84
|
+
currency, base_currency, network
|
85
|
+
)
|
86
|
+
|
87
|
+
if pc_created:
|
88
|
+
result.provider_currencies_created += 1
|
89
|
+
else:
|
90
|
+
# Update existing provider currency
|
91
|
+
updated = self._update_provider_currency_if_needed(provider_currency, currency)
|
92
|
+
if updated:
|
93
|
+
result.provider_currencies_updated += 1
|
94
|
+
|
95
|
+
def _get_or_create_currency(self, currency: UniversalCurrency) -> Tuple[Currency, bool]:
|
96
|
+
"""Get or create base currency."""
|
97
|
+
|
98
|
+
defaults = {
|
99
|
+
'name': currency.name,
|
100
|
+
'currency_type': currency.currency_type,
|
101
|
+
'is_active': currency.is_enabled,
|
102
|
+
'usd_rate': 1.0, # Will be updated by rate sync
|
103
|
+
'usd_rate_updated_at': None,
|
104
|
+
'decimal_places': self._get_decimal_places(currency),
|
105
|
+
}
|
106
|
+
|
107
|
+
return Currency.objects.get_or_create(
|
108
|
+
code=currency.base_currency_code,
|
109
|
+
defaults=defaults
|
110
|
+
)
|
111
|
+
|
112
|
+
def _update_currency_if_needed(self, base_currency: Currency, currency: UniversalCurrency) -> bool:
|
113
|
+
"""Update currency if needed."""
|
114
|
+
updated = False
|
115
|
+
|
116
|
+
# Always update name to use the proper generated name
|
117
|
+
if base_currency.name != currency.name:
|
118
|
+
base_currency.name = currency.name
|
119
|
+
updated = True
|
120
|
+
|
121
|
+
# Update activity status
|
122
|
+
if base_currency.is_active != currency.is_enabled:
|
123
|
+
base_currency.is_active = currency.is_enabled
|
124
|
+
updated = True
|
125
|
+
|
126
|
+
if updated:
|
127
|
+
base_currency.save()
|
128
|
+
|
129
|
+
return updated
|
130
|
+
|
131
|
+
def _get_or_create_network(self, currency: UniversalCurrency, base_currency: Currency) -> Tuple[Network, bool]:
|
132
|
+
"""Get or create network for currency."""
|
133
|
+
|
134
|
+
network_name = Config.get_network_name(currency.network_code)
|
135
|
+
|
136
|
+
defaults = {
|
137
|
+
'name': network_name,
|
138
|
+
'native_currency': base_currency,
|
139
|
+
'is_active': True,
|
140
|
+
'confirmation_blocks': Config.get_confirmation_blocks(currency.network_code),
|
141
|
+
}
|
142
|
+
|
143
|
+
return Network.objects.get_or_create(
|
144
|
+
code=currency.network_code,
|
145
|
+
defaults=defaults
|
146
|
+
)
|
147
|
+
|
148
|
+
def _get_or_create_provider_currency(
|
149
|
+
self,
|
150
|
+
currency: UniversalCurrency,
|
151
|
+
base_currency: Currency,
|
152
|
+
network: Optional[Network]
|
153
|
+
) -> Tuple[ProviderCurrency, bool]:
|
154
|
+
"""Get or create provider currency."""
|
155
|
+
|
156
|
+
defaults = {
|
157
|
+
'currency': base_currency,
|
158
|
+
'network': network,
|
159
|
+
'is_enabled': currency.is_enabled,
|
160
|
+
}
|
161
|
+
|
162
|
+
return ProviderCurrency.objects.get_or_create(
|
163
|
+
provider=self.provider_name,
|
164
|
+
provider_currency_code=currency.provider_currency_code,
|
165
|
+
defaults=defaults
|
166
|
+
)
|
167
|
+
|
168
|
+
def _update_provider_currency_if_needed(
|
169
|
+
self,
|
170
|
+
provider_currency: ProviderCurrency,
|
171
|
+
currency: UniversalCurrency
|
172
|
+
) -> bool:
|
173
|
+
"""Update provider currency if needed."""
|
174
|
+
updated = False
|
175
|
+
|
176
|
+
# Update enabled status
|
177
|
+
if provider_currency.is_enabled != currency.is_enabled:
|
178
|
+
provider_currency.is_enabled = currency.is_enabled
|
179
|
+
updated = True
|
180
|
+
|
181
|
+
# Update timestamps
|
182
|
+
if updated:
|
183
|
+
provider_currency.updated_at = timezone.now()
|
184
|
+
provider_currency.save()
|
185
|
+
|
186
|
+
return updated
|
187
|
+
|
188
|
+
def _get_decimal_places(self, currency: UniversalCurrency) -> int:
|
189
|
+
"""Get appropriate decimal places for currency."""
|
190
|
+
if currency.currency_type == 'fiat':
|
191
|
+
return 2
|
192
|
+
elif currency.is_stable:
|
193
|
+
return 6 # Stablecoins
|
194
|
+
else:
|
195
|
+
return 8 # Regular crypto
|
196
|
+
|
@@ -4,44 +4,16 @@ Provider registry for the Universal Payment System v2.0.
|
|
4
4
|
Centralized management of payment providers with health monitoring and fallbacks.
|
5
5
|
"""
|
6
6
|
|
7
|
-
from enum import Enum
|
8
7
|
from typing import Dict, List, Optional, Type, Any
|
9
8
|
from django_cfg.modules.django_logger import get_logger
|
10
9
|
# ConfigService removed - using direct Constance access
|
11
10
|
from ..types import ServiceOperationResult
|
12
|
-
from .base import BaseProvider
|
13
|
-
from .
|
11
|
+
from .base import BaseProvider
|
12
|
+
from .models import ProviderConfig, ProviderEnum
|
13
|
+
from .nowpayments import NowPaymentsProvider, NowPaymentsProviderConfig
|
14
14
|
|
15
15
|
logger = get_logger("provider_registry")
|
16
16
|
|
17
|
-
|
18
|
-
# Provider enums
|
19
|
-
class ProviderEnum(Enum):
|
20
|
-
NOWPAYMENTS = "nowpayments"
|
21
|
-
CRYPTAPI = "cryptapi"
|
22
|
-
STRIPE = "stripe"
|
23
|
-
CRYPTOMUS = "cryptomus"
|
24
|
-
|
25
|
-
@classmethod
|
26
|
-
def get_crypto_providers(cls):
|
27
|
-
"""Get list of crypto provider values."""
|
28
|
-
return [cls.NOWPAYMENTS.value, cls.CRYPTAPI.value, cls.CRYPTOMUS.value]
|
29
|
-
|
30
|
-
@classmethod
|
31
|
-
def get_fiat_providers(cls):
|
32
|
-
"""Get list of fiat provider values."""
|
33
|
-
return [cls.STRIPE.value]
|
34
|
-
|
35
|
-
@classmethod
|
36
|
-
def get_priority_order(cls):
|
37
|
-
"""Get list of provider values in priority order."""
|
38
|
-
return [
|
39
|
-
cls.NOWPAYMENTS.value,
|
40
|
-
cls.CRYPTAPI.value,
|
41
|
-
cls.STRIPE.value,
|
42
|
-
cls.CRYPTOMUS.value
|
43
|
-
]
|
44
|
-
|
45
17
|
class ProviderRegistry:
|
46
18
|
"""
|
47
19
|
Registry for managing payment providers.
|
@@ -52,17 +24,17 @@ class ProviderRegistry:
|
|
52
24
|
|
53
25
|
def __init__(self):
|
54
26
|
"""Initialize provider registry."""
|
55
|
-
# Use
|
56
|
-
from ...config.
|
27
|
+
# Use PaymentsConfigManager for configuration from BaseCfgAutoModule
|
28
|
+
from ...config.django_cfg_integration import PaymentsConfigManager
|
57
29
|
|
58
|
-
self.
|
30
|
+
self.config_manager = PaymentsConfigManager
|
59
31
|
self._providers: Dict[str, BaseProvider] = {}
|
60
32
|
|
61
33
|
self._provider_classes: Dict[str, Type[BaseProvider]] = {
|
62
34
|
ProviderEnum.NOWPAYMENTS.value: NowPaymentsProvider,
|
63
35
|
}
|
64
36
|
self._provider_configs: Dict[str, Type[ProviderConfig]] = {
|
65
|
-
ProviderEnum.NOWPAYMENTS.value:
|
37
|
+
ProviderEnum.NOWPAYMENTS.value: NowPaymentsProviderConfig,
|
66
38
|
}
|
67
39
|
|
68
40
|
self._health_status: Dict[str, bool] = {}
|
@@ -78,9 +50,9 @@ class ProviderRegistry:
|
|
78
50
|
try:
|
79
51
|
logger.info("Initializing provider registry")
|
80
52
|
|
81
|
-
# Get all provider configurations
|
53
|
+
# Get all provider configurations from BaseCfgAutoModule
|
82
54
|
try:
|
83
|
-
provider_configs = self.
|
55
|
+
provider_configs = self.config_manager.get_all_provider_configs()
|
84
56
|
except Exception as e:
|
85
57
|
return ServiceOperationResult(
|
86
58
|
success=False,
|
@@ -0,0 +1,277 @@
|
|
1
|
+
"""
|
2
|
+
Universal provider synchronization service.
|
3
|
+
|
4
|
+
Handles synchronization of currencies from all payment providers to database.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import List, Dict, Optional
|
8
|
+
from django.db import transaction
|
9
|
+
from django.utils import timezone
|
10
|
+
from datetime import timedelta
|
11
|
+
|
12
|
+
from django_cfg.modules.django_logger import get_logger
|
13
|
+
from django_cfg.apps.payments.models import ProviderCurrency
|
14
|
+
from .models import CurrencySyncResult
|
15
|
+
from .registry import get_provider_registry
|
16
|
+
from .base import BaseProvider
|
17
|
+
|
18
|
+
logger = get_logger("provider_sync")
|
19
|
+
|
20
|
+
|
21
|
+
class ProviderSyncService:
|
22
|
+
"""Universal service for synchronizing all payment providers."""
|
23
|
+
|
24
|
+
def __init__(self):
|
25
|
+
"""Initialize provider sync service."""
|
26
|
+
self.registry = get_provider_registry()
|
27
|
+
|
28
|
+
def sync_all_providers(
|
29
|
+
self,
|
30
|
+
force_refresh: bool = False,
|
31
|
+
provider_names: Optional[List[str]] = None
|
32
|
+
) -> Dict[str, CurrencySyncResult]:
|
33
|
+
"""
|
34
|
+
Sync currencies from all available providers.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
force_refresh: Force refresh even if recently synced
|
38
|
+
provider_names: Specific providers to sync (None = all available)
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
Dict[str, CurrencySyncResult]: Results by provider name
|
42
|
+
"""
|
43
|
+
logger.info("Starting universal provider synchronization")
|
44
|
+
|
45
|
+
# Get providers to sync
|
46
|
+
if provider_names:
|
47
|
+
providers_to_sync = []
|
48
|
+
for name in provider_names:
|
49
|
+
provider = self.registry.get_provider(name)
|
50
|
+
if provider:
|
51
|
+
providers_to_sync.append((name, provider))
|
52
|
+
else:
|
53
|
+
logger.warning(f"Provider {name} not available")
|
54
|
+
else:
|
55
|
+
available_providers = self.registry.get_available_providers()
|
56
|
+
providers_to_sync = [
|
57
|
+
(name, self.registry.get_provider(name))
|
58
|
+
for name in available_providers
|
59
|
+
]
|
60
|
+
|
61
|
+
if not providers_to_sync:
|
62
|
+
logger.warning("No providers available for synchronization")
|
63
|
+
return {}
|
64
|
+
|
65
|
+
# Sync each provider
|
66
|
+
results = {}
|
67
|
+
total_currencies = 0
|
68
|
+
total_errors = 0
|
69
|
+
|
70
|
+
for provider_name, provider in providers_to_sync:
|
71
|
+
try:
|
72
|
+
logger.info(f"Syncing provider: {provider_name}")
|
73
|
+
|
74
|
+
# Check if sync is needed
|
75
|
+
if not force_refresh and self._is_recently_synced(provider_name):
|
76
|
+
logger.info(f"Provider {provider_name} recently synced, skipping")
|
77
|
+
results[provider_name] = CurrencySyncResult(
|
78
|
+
total_processed=0,
|
79
|
+
errors=[f"Skipped - recently synced (use --force-refresh to override)"]
|
80
|
+
)
|
81
|
+
continue
|
82
|
+
|
83
|
+
# Perform sync
|
84
|
+
sync_result = self._sync_single_provider(provider)
|
85
|
+
results[provider_name] = sync_result
|
86
|
+
|
87
|
+
# Update stats
|
88
|
+
total_currencies += sync_result.currencies_created + sync_result.currencies_updated
|
89
|
+
total_errors += len(sync_result.errors)
|
90
|
+
|
91
|
+
# Mark sync time
|
92
|
+
self._mark_sync_time(provider_name)
|
93
|
+
|
94
|
+
logger.info(
|
95
|
+
f"Provider {provider_name} sync completed: "
|
96
|
+
f"{sync_result.currencies_created} created, "
|
97
|
+
f"{sync_result.currencies_updated} updated, "
|
98
|
+
f"{len(sync_result.errors)} errors"
|
99
|
+
)
|
100
|
+
|
101
|
+
except Exception as e:
|
102
|
+
error_msg = f"Provider {provider_name} sync failed: {e}"
|
103
|
+
logger.error(error_msg)
|
104
|
+
results[provider_name] = CurrencySyncResult(
|
105
|
+
total_processed=0,
|
106
|
+
errors=[error_msg]
|
107
|
+
)
|
108
|
+
total_errors += 1
|
109
|
+
|
110
|
+
logger.info(
|
111
|
+
f"Universal provider sync completed: "
|
112
|
+
f"{len(results)} providers processed, "
|
113
|
+
f"{total_currencies} currencies synced, "
|
114
|
+
f"{total_errors} errors"
|
115
|
+
)
|
116
|
+
|
117
|
+
return results
|
118
|
+
|
119
|
+
def sync_provider(
|
120
|
+
self,
|
121
|
+
provider_name: str,
|
122
|
+
force_refresh: bool = False
|
123
|
+
) -> CurrencySyncResult:
|
124
|
+
"""
|
125
|
+
Sync currencies from a specific provider.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
provider_name: Name of provider to sync
|
129
|
+
force_refresh: Force refresh even if recently synced
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
CurrencySyncResult: Sync operation result
|
133
|
+
"""
|
134
|
+
provider = self.registry.get_provider(provider_name)
|
135
|
+
if not provider:
|
136
|
+
return CurrencySyncResult(
|
137
|
+
total_processed=0,
|
138
|
+
errors=[f"Provider {provider_name} not available"]
|
139
|
+
)
|
140
|
+
|
141
|
+
# Check if sync is needed
|
142
|
+
if not force_refresh and self._is_recently_synced(provider_name):
|
143
|
+
return CurrencySyncResult(
|
144
|
+
total_processed=0,
|
145
|
+
errors=[f"Provider {provider_name} recently synced (use force_refresh=True to override)"]
|
146
|
+
)
|
147
|
+
|
148
|
+
# Perform sync
|
149
|
+
result = self._sync_single_provider(provider)
|
150
|
+
|
151
|
+
# Mark sync time
|
152
|
+
self._mark_sync_time(provider_name)
|
153
|
+
|
154
|
+
return result
|
155
|
+
|
156
|
+
def get_sync_statistics(self) -> Dict[str, any]:
|
157
|
+
"""
|
158
|
+
Get synchronization statistics for all providers.
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
Dict: Statistics by provider
|
162
|
+
"""
|
163
|
+
stats = {}
|
164
|
+
|
165
|
+
for provider_name in self.registry.list_providers():
|
166
|
+
provider_stats = self._get_provider_stats(provider_name)
|
167
|
+
stats[provider_name] = provider_stats
|
168
|
+
|
169
|
+
return stats
|
170
|
+
|
171
|
+
def _sync_single_provider(self, provider: BaseProvider) -> CurrencySyncResult:
|
172
|
+
"""Sync currencies from a single provider."""
|
173
|
+
try:
|
174
|
+
logger.debug(f"Starting sync for provider: {provider.name}")
|
175
|
+
|
176
|
+
# Use provider's sync method
|
177
|
+
result = provider.sync_currencies_to_db()
|
178
|
+
|
179
|
+
logger.debug(
|
180
|
+
f"Provider {provider.name} sync result: "
|
181
|
+
f"{result.currencies_created} created, "
|
182
|
+
f"{result.currencies_updated} updated, "
|
183
|
+
f"{result.provider_currencies_created} provider currencies created"
|
184
|
+
)
|
185
|
+
|
186
|
+
return result
|
187
|
+
|
188
|
+
except Exception as e:
|
189
|
+
error_msg = f"Sync failed for provider {provider.name}: {e}"
|
190
|
+
logger.error(error_msg)
|
191
|
+
return CurrencySyncResult(
|
192
|
+
total_processed=0,
|
193
|
+
errors=[error_msg]
|
194
|
+
)
|
195
|
+
|
196
|
+
def _is_recently_synced(self, provider_name: str, hours: int = 1) -> bool:
|
197
|
+
"""Check if provider was recently synced."""
|
198
|
+
try:
|
199
|
+
# Check if any provider currencies were updated recently
|
200
|
+
recent_threshold = timezone.now() - timedelta(hours=hours)
|
201
|
+
|
202
|
+
recent_updates = ProviderCurrency.objects.filter(
|
203
|
+
provider=provider_name,
|
204
|
+
updated_at__gte=recent_threshold
|
205
|
+
).exists()
|
206
|
+
|
207
|
+
return recent_updates
|
208
|
+
|
209
|
+
except Exception as e:
|
210
|
+
logger.warning(f"Failed to check sync status for {provider_name}: {e}")
|
211
|
+
return False
|
212
|
+
|
213
|
+
def _mark_sync_time(self, provider_name: str):
|
214
|
+
"""Mark sync time for provider (could be stored in cache/database)."""
|
215
|
+
# For now, we rely on ProviderCurrency.updated_at
|
216
|
+
# In future, could store in dedicated sync log table
|
217
|
+
pass
|
218
|
+
|
219
|
+
def _get_provider_stats(self, provider_name: str) -> Dict[str, any]:
|
220
|
+
"""Get statistics for a specific provider."""
|
221
|
+
try:
|
222
|
+
from django.db.models import Count, Q
|
223
|
+
|
224
|
+
# Get provider currency stats
|
225
|
+
provider_currencies = ProviderCurrency.objects.filter(provider=provider_name)
|
226
|
+
|
227
|
+
total_currencies = provider_currencies.count()
|
228
|
+
enabled_currencies = provider_currencies.filter(is_enabled=True).count()
|
229
|
+
|
230
|
+
# Get recent activity
|
231
|
+
recent_threshold = timezone.now() - timedelta(hours=24)
|
232
|
+
recent_updates = provider_currencies.filter(
|
233
|
+
updated_at__gte=recent_threshold
|
234
|
+
).count()
|
235
|
+
|
236
|
+
# Get last sync time
|
237
|
+
last_sync = None
|
238
|
+
if provider_currencies.exists():
|
239
|
+
last_sync = provider_currencies.order_by('-updated_at').first().updated_at
|
240
|
+
|
241
|
+
return {
|
242
|
+
'total_currencies': total_currencies,
|
243
|
+
'enabled_currencies': enabled_currencies,
|
244
|
+
'disabled_currencies': total_currencies - enabled_currencies,
|
245
|
+
'recent_updates_24h': recent_updates,
|
246
|
+
'last_sync': last_sync.isoformat() if last_sync else None,
|
247
|
+
'is_recently_synced': self._is_recently_synced(provider_name)
|
248
|
+
}
|
249
|
+
|
250
|
+
except Exception as e:
|
251
|
+
logger.error(f"Failed to get stats for {provider_name}: {e}")
|
252
|
+
return {
|
253
|
+
'error': str(e),
|
254
|
+
'total_currencies': 0,
|
255
|
+
'enabled_currencies': 0,
|
256
|
+
'disabled_currencies': 0,
|
257
|
+
'recent_updates_24h': 0,
|
258
|
+
'last_sync': None,
|
259
|
+
'is_recently_synced': False
|
260
|
+
}
|
261
|
+
|
262
|
+
|
263
|
+
# Global sync service instance
|
264
|
+
_global_sync_service: Optional[ProviderSyncService] = None
|
265
|
+
|
266
|
+
|
267
|
+
def get_provider_sync_service() -> ProviderSyncService:
|
268
|
+
"""
|
269
|
+
Get global provider sync service instance.
|
270
|
+
|
271
|
+
Returns:
|
272
|
+
ProviderSyncService: Global sync service instance
|
273
|
+
"""
|
274
|
+
global _global_sync_service
|
275
|
+
if _global_sync_service is None:
|
276
|
+
_global_sync_service = ProviderSyncService()
|
277
|
+
return _global_sync_service
|
@@ -24,8 +24,8 @@ class PaymentCreateRequest(BaseModel):
|
|
24
24
|
|
25
25
|
user_id: int = Field(gt=0, description="User ID")
|
26
26
|
amount_usd: float = Field(gt=1.0, le=50000.0, description="Amount in USD")
|
27
|
-
currency_code:
|
28
|
-
description="Cryptocurrency code"
|
27
|
+
currency_code: str = Field(
|
28
|
+
min_length=2, max_length=10, description="Cryptocurrency code"
|
29
29
|
)
|
30
30
|
provider: Literal['nowpayments'] = Field(default='nowpayments', description="Payment provider")
|
31
31
|
callback_url: Optional[str] = Field(None, description="Success callback URL")
|
@@ -36,11 +36,23 @@ class PaymentCreateRequest(BaseModel):
|
|
36
36
|
@field_validator('currency_code')
|
37
37
|
@classmethod
|
38
38
|
def validate_currency(cls, v: str) -> str:
|
39
|
-
"""Validate currency is supported."""
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
"""Validate currency is supported by checking database."""
|
40
|
+
from django_cfg.apps.payments.models import Currency
|
41
|
+
|
42
|
+
currency_code = v.upper().strip()
|
43
|
+
|
44
|
+
# Check if currency exists in database and is active
|
45
|
+
try:
|
46
|
+
currency = Currency.objects.get(code=currency_code, is_active=True)
|
47
|
+
return currency_code
|
48
|
+
except Currency.DoesNotExist:
|
49
|
+
# Get list of active currencies for error message
|
50
|
+
active_currencies = Currency.objects.filter(is_active=True).values_list('code', flat=True)[:10]
|
51
|
+
currency_list = ', '.join(active_currencies) + ('...' if len(active_currencies) == 10 else '')
|
52
|
+
raise ValueError(f"Currency {currency_code} not found or inactive. Available: {currency_list}")
|
53
|
+
except Exception:
|
54
|
+
# Fallback validation if database is not available
|
55
|
+
return currency_code
|
44
56
|
|
45
57
|
@field_validator('callback_url', 'cancel_url')
|
46
58
|
@classmethod
|
@@ -17,16 +17,45 @@ logger = get_logger("payment_signals")
|
|
17
17
|
|
18
18
|
|
19
19
|
@receiver(pre_save, sender=UniversalPayment)
|
20
|
-
def
|
21
|
-
"""
|
20
|
+
def handle_status_changes(sender, instance: UniversalPayment, **kwargs):
|
21
|
+
"""
|
22
|
+
Handle status changes and update status_changed_at field.
|
23
|
+
|
24
|
+
This signal automatically updates status_changed_at when status changes,
|
25
|
+
ensuring consistent tracking across all update methods.
|
26
|
+
"""
|
22
27
|
if instance.pk:
|
23
28
|
try:
|
24
29
|
original = UniversalPayment.objects.get(pk=instance.pk)
|
25
30
|
instance._original_status = original.status
|
31
|
+
|
32
|
+
# Check if status has changed
|
33
|
+
if original.status != instance.status:
|
34
|
+
instance.status_changed_at = timezone.now()
|
35
|
+
|
36
|
+
# Set completed_at if status changed to completed
|
37
|
+
if instance.status == 'completed' and not instance.completed_at:
|
38
|
+
instance.completed_at = timezone.now()
|
39
|
+
|
40
|
+
logger.debug(f"Status change detected in pre_save", extra={
|
41
|
+
'payment_id': str(instance.id),
|
42
|
+
'old_status': original.status,
|
43
|
+
'new_status': instance.status,
|
44
|
+
'status_changed_at': instance.status_changed_at.isoformat()
|
45
|
+
})
|
26
46
|
except UniversalPayment.DoesNotExist:
|
27
47
|
instance._original_status = None
|
28
48
|
else:
|
49
|
+
# New object - set status_changed_at if status is set
|
29
50
|
instance._original_status = None
|
51
|
+
if instance.status and not instance.status_changed_at:
|
52
|
+
instance.status_changed_at = timezone.now()
|
53
|
+
|
54
|
+
logger.debug(f"New payment status set in pre_save", extra={
|
55
|
+
'payment_id': 'new',
|
56
|
+
'status': instance.status,
|
57
|
+
'status_changed_at': instance.status_changed_at.isoformat()
|
58
|
+
})
|
30
59
|
|
31
60
|
|
32
61
|
@receiver(post_save, sender=UniversalPayment)
|