django-cfg 1.2.31__py3-none-any.whl → 1.3.1__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/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -10
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +526 -222
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +465 -70
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
- django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
- django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +13 -18
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +172 -148
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -285
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +346 -467
- django_cfg/apps/payments/services/core/subscription_service.py +425 -481
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +234 -174
- django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
- django_cfg/apps/payments/services/providers/registry.py +367 -301
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +210 -129
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -103
- django_cfg/apps/payments/signals/subscription_signals.py +194 -142
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +45 -48
- django_cfg/apps/payments/urls_admin.py +33 -42
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +40 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -492
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +64 -16
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -146
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
- django_cfg/apps/payments/managers/__init__.py +0 -23
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -306
- django_cfg/apps/payments/managers/payment_manager.py +0 -192
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -57
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -63
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -461
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
- django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
- django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
- django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
- django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
- django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -635
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
- django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
- django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
- django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
- django_cfg/apps/payments/templates/payments/stats.html +0 -261
- django_cfg/apps/payments/templates/payments/test.html +0 -213
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/utils/__init__.py +0 -43
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -239
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -63
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -122
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -451
- django_cfg/apps/payments/views/templates/base.py +0 -212
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -158
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -244
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -66
- django_cfg/core/integration.py +0 -160
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,361 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
User balance manager with atomic operations.
|
3
|
-
|
4
|
-
Following CRITICAL_REQUIREMENTS.md:
|
5
|
-
- Atomic balance updates
|
6
|
-
- Type safety
|
7
|
-
- Event sourcing
|
8
|
-
- Proper error handling
|
9
|
-
"""
|
10
|
-
|
11
|
-
from django.db import models, transaction
|
12
|
-
from django.utils import timezone
|
13
|
-
from decimal import Decimal
|
14
|
-
from typing import Optional, Dict, Any
|
15
|
-
from django_cfg.modules.django_logger import get_logger
|
16
|
-
|
17
|
-
logger = get_logger("balance_manager")
|
18
|
-
|
19
|
-
|
20
|
-
class UserBalanceManager(models.Manager):
|
21
|
-
"""Manager for UserBalance with atomic operations."""
|
22
|
-
|
23
|
-
def get_or_create_balance(self, user) -> 'UserBalance':
|
24
|
-
"""Get or create user balance atomically."""
|
25
|
-
balance, created = self.get_or_create(
|
26
|
-
user=user,
|
27
|
-
defaults={
|
28
|
-
'amount_usd': Decimal('0'),
|
29
|
-
'reserved_usd': Decimal('0'),
|
30
|
-
'total_earned': Decimal('0'),
|
31
|
-
'total_spent': Decimal('0'),
|
32
|
-
}
|
33
|
-
)
|
34
|
-
|
35
|
-
if created:
|
36
|
-
logger.info(f"Created new balance for user {user.id}")
|
37
|
-
|
38
|
-
return balance
|
39
|
-
|
40
|
-
def add_funds(
|
41
|
-
self,
|
42
|
-
user,
|
43
|
-
amount_usd: Decimal,
|
44
|
-
description: str,
|
45
|
-
reference_id: Optional[str] = None,
|
46
|
-
payment=None
|
47
|
-
) -> Dict[str, Any]:
|
48
|
-
"""
|
49
|
-
Add funds to user balance atomically.
|
50
|
-
|
51
|
-
Returns:
|
52
|
-
Dict with operation result and transaction details
|
53
|
-
"""
|
54
|
-
if amount_usd <= 0:
|
55
|
-
raise ValueError("Amount must be positive")
|
56
|
-
|
57
|
-
with transaction.atomic():
|
58
|
-
# Get or create balance with row lock
|
59
|
-
balance = self.select_for_update().get_or_create_balance(user)
|
60
|
-
|
61
|
-
# Store old values for transaction record
|
62
|
-
old_balance = balance.amount_usd
|
63
|
-
old_earned = balance.total_earned
|
64
|
-
|
65
|
-
# Update balance
|
66
|
-
balance.amount_usd += amount_usd
|
67
|
-
balance.total_earned += amount_usd
|
68
|
-
balance.last_transaction_at = timezone.now()
|
69
|
-
balance.save()
|
70
|
-
|
71
|
-
# Create transaction record
|
72
|
-
from ..models.balance import Transaction
|
73
|
-
transaction_record = Transaction.objects.create(
|
74
|
-
user=user,
|
75
|
-
amount_usd=amount_usd,
|
76
|
-
transaction_type=Transaction.TypeChoices.CREDIT,
|
77
|
-
description=description,
|
78
|
-
payment=payment,
|
79
|
-
reference_id=reference_id,
|
80
|
-
balance_before=old_balance,
|
81
|
-
balance_after=balance.amount_usd,
|
82
|
-
metadata={
|
83
|
-
'total_earned_before': str(old_earned),
|
84
|
-
'total_earned_after': str(balance.total_earned),
|
85
|
-
}
|
86
|
-
)
|
87
|
-
|
88
|
-
logger.info(
|
89
|
-
f"Added ${amount_usd} to user {user.id} balance. "
|
90
|
-
f"New balance: ${balance.amount_usd}"
|
91
|
-
)
|
92
|
-
|
93
|
-
return {
|
94
|
-
'success': True,
|
95
|
-
'old_balance': old_balance,
|
96
|
-
'new_balance': balance.amount_usd,
|
97
|
-
'amount_added': amount_usd,
|
98
|
-
'transaction_id': str(transaction_record.id),
|
99
|
-
'balance_obj': balance
|
100
|
-
}
|
101
|
-
|
102
|
-
def debit_funds(
|
103
|
-
self,
|
104
|
-
user,
|
105
|
-
amount_usd: Decimal,
|
106
|
-
description: str,
|
107
|
-
reference_id: Optional[str] = None,
|
108
|
-
allow_overdraft: bool = False
|
109
|
-
) -> Dict[str, Any]:
|
110
|
-
"""
|
111
|
-
Debit funds from user balance atomically.
|
112
|
-
|
113
|
-
Args:
|
114
|
-
user: User object
|
115
|
-
amount_usd: Amount to debit (positive value)
|
116
|
-
description: Transaction description
|
117
|
-
reference_id: Optional reference ID
|
118
|
-
allow_overdraft: Allow negative balance
|
119
|
-
|
120
|
-
Returns:
|
121
|
-
Dict with operation result
|
122
|
-
"""
|
123
|
-
if amount_usd <= 0:
|
124
|
-
raise ValueError("Amount must be positive")
|
125
|
-
|
126
|
-
with transaction.atomic():
|
127
|
-
# Get balance with row lock
|
128
|
-
balance = self.select_for_update().get_or_create_balance(user)
|
129
|
-
|
130
|
-
# Check sufficient funds
|
131
|
-
if not allow_overdraft and balance.amount_usd < amount_usd:
|
132
|
-
from ..models.exceptions import InsufficientFundsError
|
133
|
-
from ..models.pydantic_models import MoneyAmount
|
134
|
-
from ..models import CurrencyChoices
|
135
|
-
|
136
|
-
raise InsufficientFundsError(
|
137
|
-
message=f"Insufficient funds: ${balance.amount_usd} < ${amount_usd}",
|
138
|
-
required_amount=MoneyAmount(amount=amount_usd, currency=CurrencyChoices.USD),
|
139
|
-
available_amount=MoneyAmount(amount=balance.amount_usd, currency=CurrencyChoices.USD),
|
140
|
-
user_id=user.id
|
141
|
-
)
|
142
|
-
|
143
|
-
# Store old values
|
144
|
-
old_balance = balance.amount_usd
|
145
|
-
old_spent = balance.total_spent
|
146
|
-
|
147
|
-
# Update balance
|
148
|
-
balance.amount_usd -= amount_usd
|
149
|
-
balance.total_spent += amount_usd
|
150
|
-
balance.last_transaction_at = timezone.now()
|
151
|
-
balance.save()
|
152
|
-
|
153
|
-
# Create transaction record
|
154
|
-
from ..models.balance import Transaction
|
155
|
-
transaction_record = Transaction.objects.create(
|
156
|
-
user=user,
|
157
|
-
amount_usd=-amount_usd, # Negative for debit
|
158
|
-
transaction_type=Transaction.TypeChoices.DEBIT,
|
159
|
-
description=description,
|
160
|
-
reference_id=reference_id,
|
161
|
-
balance_before=old_balance,
|
162
|
-
balance_after=balance.amount_usd,
|
163
|
-
metadata={
|
164
|
-
'total_spent_before': str(old_spent),
|
165
|
-
'total_spent_after': str(balance.total_spent),
|
166
|
-
'allow_overdraft': allow_overdraft,
|
167
|
-
}
|
168
|
-
)
|
169
|
-
|
170
|
-
logger.info(
|
171
|
-
f"Debited ${amount_usd} from user {user.id} balance. "
|
172
|
-
f"New balance: ${balance.amount_usd}"
|
173
|
-
)
|
174
|
-
|
175
|
-
return {
|
176
|
-
'success': True,
|
177
|
-
'old_balance': old_balance,
|
178
|
-
'new_balance': balance.amount_usd,
|
179
|
-
'amount_debited': amount_usd,
|
180
|
-
'transaction_id': str(transaction_record.id),
|
181
|
-
'balance_obj': balance
|
182
|
-
}
|
183
|
-
|
184
|
-
def hold_funds(
|
185
|
-
self,
|
186
|
-
user,
|
187
|
-
amount_usd: Decimal,
|
188
|
-
description: str,
|
189
|
-
reference_id: Optional[str] = None
|
190
|
-
) -> Dict[str, Any]:
|
191
|
-
"""
|
192
|
-
Hold funds (move from available to reserved).
|
193
|
-
|
194
|
-
Args:
|
195
|
-
user: User object
|
196
|
-
amount_usd: Amount to hold
|
197
|
-
description: Hold description
|
198
|
-
reference_id: Optional reference ID
|
199
|
-
|
200
|
-
Returns:
|
201
|
-
Dict with operation result
|
202
|
-
"""
|
203
|
-
if amount_usd <= 0:
|
204
|
-
raise ValueError("Amount must be positive")
|
205
|
-
|
206
|
-
with transaction.atomic():
|
207
|
-
# Get balance with row lock
|
208
|
-
balance = self.select_for_update().get_or_create_balance(user)
|
209
|
-
|
210
|
-
# Check sufficient available funds
|
211
|
-
if balance.amount_usd < amount_usd:
|
212
|
-
from ..models.exceptions import InsufficientFundsError
|
213
|
-
from ..models.pydantic_models import MoneyAmount
|
214
|
-
from ..models import CurrencyChoices
|
215
|
-
|
216
|
-
raise InsufficientFundsError(
|
217
|
-
message=f"Insufficient available funds for hold: ${balance.amount_usd} < ${amount_usd}",
|
218
|
-
required_amount=MoneyAmount(amount=amount_usd, currency=CurrencyChoices.USD),
|
219
|
-
available_amount=MoneyAmount(amount=balance.amount_usd, currency=CurrencyChoices.USD),
|
220
|
-
user_id=user.id
|
221
|
-
)
|
222
|
-
|
223
|
-
# Store old values
|
224
|
-
old_available = balance.amount_usd
|
225
|
-
old_reserved = balance.reserved_usd
|
226
|
-
|
227
|
-
# Move funds from available to reserved
|
228
|
-
balance.amount_usd -= amount_usd
|
229
|
-
balance.reserved_usd += amount_usd
|
230
|
-
balance.last_transaction_at = timezone.now()
|
231
|
-
balance.save()
|
232
|
-
|
233
|
-
# Create transaction record
|
234
|
-
from ..models.balance import Transaction
|
235
|
-
transaction_record = Transaction.objects.create(
|
236
|
-
user=user,
|
237
|
-
amount_usd=amount_usd,
|
238
|
-
transaction_type=Transaction.TypeChoices.HOLD,
|
239
|
-
description=description,
|
240
|
-
reference_id=reference_id,
|
241
|
-
balance_before=old_available,
|
242
|
-
balance_after=balance.amount_usd,
|
243
|
-
metadata={
|
244
|
-
'reserved_before': str(old_reserved),
|
245
|
-
'reserved_after': str(balance.reserved_usd),
|
246
|
-
'operation': 'hold_funds',
|
247
|
-
}
|
248
|
-
)
|
249
|
-
|
250
|
-
logger.info(
|
251
|
-
f"Held ${amount_usd} for user {user.id}. "
|
252
|
-
f"Available: ${balance.amount_usd}, Reserved: ${balance.reserved_usd}"
|
253
|
-
)
|
254
|
-
|
255
|
-
return {
|
256
|
-
'success': True,
|
257
|
-
'amount_held': amount_usd,
|
258
|
-
'available_balance': balance.amount_usd,
|
259
|
-
'reserved_balance': balance.reserved_usd,
|
260
|
-
'transaction_id': str(transaction_record.id),
|
261
|
-
'balance_obj': balance
|
262
|
-
}
|
263
|
-
|
264
|
-
def release_funds(
|
265
|
-
self,
|
266
|
-
user,
|
267
|
-
amount_usd: Decimal,
|
268
|
-
description: str,
|
269
|
-
reference_id: Optional[str] = None,
|
270
|
-
refund_to_available: bool = True
|
271
|
-
) -> Dict[str, Any]:
|
272
|
-
"""
|
273
|
-
Release held funds.
|
274
|
-
|
275
|
-
Args:
|
276
|
-
user: User object
|
277
|
-
amount_usd: Amount to release
|
278
|
-
description: Release description
|
279
|
-
reference_id: Optional reference ID
|
280
|
-
refund_to_available: If True, move to available; if False, remove entirely
|
281
|
-
|
282
|
-
Returns:
|
283
|
-
Dict with operation result
|
284
|
-
"""
|
285
|
-
if amount_usd <= 0:
|
286
|
-
raise ValueError("Amount must be positive")
|
287
|
-
|
288
|
-
with transaction.atomic():
|
289
|
-
# Get balance with row lock
|
290
|
-
balance = self.select_for_update().get_or_create_balance(user)
|
291
|
-
|
292
|
-
# Check sufficient reserved funds
|
293
|
-
if balance.reserved_usd < amount_usd:
|
294
|
-
raise ValueError(
|
295
|
-
f"Insufficient reserved funds: ${balance.reserved_usd} < ${amount_usd}"
|
296
|
-
)
|
297
|
-
|
298
|
-
# Store old values
|
299
|
-
old_available = balance.amount_usd
|
300
|
-
old_reserved = balance.reserved_usd
|
301
|
-
|
302
|
-
# Release funds
|
303
|
-
balance.reserved_usd -= amount_usd
|
304
|
-
if refund_to_available:
|
305
|
-
balance.amount_usd += amount_usd
|
306
|
-
else:
|
307
|
-
# Funds are consumed (e.g., for payment)
|
308
|
-
balance.total_spent += amount_usd
|
309
|
-
|
310
|
-
balance.last_transaction_at = timezone.now()
|
311
|
-
balance.save()
|
312
|
-
|
313
|
-
# Create transaction record
|
314
|
-
from ..models.balance import Transaction
|
315
|
-
transaction_record = Transaction.objects.create(
|
316
|
-
user=user,
|
317
|
-
amount_usd=amount_usd if refund_to_available else -amount_usd,
|
318
|
-
transaction_type=Transaction.TypeChoices.RELEASE,
|
319
|
-
description=description,
|
320
|
-
reference_id=reference_id,
|
321
|
-
balance_before=old_available,
|
322
|
-
balance_after=balance.amount_usd,
|
323
|
-
metadata={
|
324
|
-
'reserved_before': str(old_reserved),
|
325
|
-
'reserved_after': str(balance.reserved_usd),
|
326
|
-
'refund_to_available': refund_to_available,
|
327
|
-
'operation': 'release_funds',
|
328
|
-
}
|
329
|
-
)
|
330
|
-
|
331
|
-
action = "refunded to available" if refund_to_available else "consumed"
|
332
|
-
logger.info(
|
333
|
-
f"Released ${amount_usd} for user {user.id} ({action}). "
|
334
|
-
f"Available: ${balance.amount_usd}, Reserved: ${balance.reserved_usd}"
|
335
|
-
)
|
336
|
-
|
337
|
-
return {
|
338
|
-
'success': True,
|
339
|
-
'amount_released': amount_usd,
|
340
|
-
'refund_to_available': refund_to_available,
|
341
|
-
'available_balance': balance.amount_usd,
|
342
|
-
'reserved_balance': balance.reserved_usd,
|
343
|
-
'transaction_id': str(transaction_record.id),
|
344
|
-
'balance_obj': balance
|
345
|
-
}
|
346
|
-
|
347
|
-
def get_balance_summary(self, user) -> Dict[str, Any]:
|
348
|
-
"""Get comprehensive balance summary for user."""
|
349
|
-
balance = self.get_or_create_balance(user)
|
350
|
-
|
351
|
-
return {
|
352
|
-
'user_id': user.id,
|
353
|
-
'available_balance': balance.amount_usd,
|
354
|
-
'reserved_balance': balance.reserved_usd,
|
355
|
-
'total_balance': balance.total_balance,
|
356
|
-
'total_earned': balance.total_earned,
|
357
|
-
'total_spent': balance.total_spent,
|
358
|
-
'last_transaction_at': balance.last_transaction_at,
|
359
|
-
'created_at': balance.created_at,
|
360
|
-
'updated_at': balance.updated_at,
|
361
|
-
}
|
@@ -1,306 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Manager for Currency model.
|
3
|
-
"""
|
4
|
-
|
5
|
-
from django.db import models
|
6
|
-
from django.utils import timezone
|
7
|
-
from datetime import timedelta
|
8
|
-
from typing import List, Optional, TYPE_CHECKING
|
9
|
-
from decimal import Decimal
|
10
|
-
|
11
|
-
from django_cfg.modules.django_logger import get_logger
|
12
|
-
from django_cfg.modules.django_currency import convert_currency, get_exchange_rate, CurrencyError
|
13
|
-
|
14
|
-
if TYPE_CHECKING:
|
15
|
-
from ..services.internal_types import CurrencyOptionModel
|
16
|
-
|
17
|
-
logger = get_logger("currency_manager")
|
18
|
-
|
19
|
-
|
20
|
-
class CurrencyManager(models.Manager):
|
21
|
-
"""Manager for clean Currency model."""
|
22
|
-
|
23
|
-
def fiat(self):
|
24
|
-
"""Get only fiat currencies."""
|
25
|
-
return self.filter(currency_type='fiat')
|
26
|
-
|
27
|
-
def crypto(self):
|
28
|
-
"""Get only cryptocurrencies."""
|
29
|
-
return self.filter(currency_type='crypto')
|
30
|
-
|
31
|
-
def by_code(self, code: str):
|
32
|
-
"""Get currency by code (case insensitive)."""
|
33
|
-
return self.filter(code__iexact=code).first()
|
34
|
-
|
35
|
-
def search(self, query: str):
|
36
|
-
"""Search currencies by code or name."""
|
37
|
-
return self.filter(
|
38
|
-
models.Q(code__icontains=query) |
|
39
|
-
models.Q(name__icontains=query)
|
40
|
-
)
|
41
|
-
|
42
|
-
def get_usd_rate(self, currency_code_or_instance, force_refresh: bool = False) -> float:
|
43
|
-
"""
|
44
|
-
Get USD exchange rate for currency (with 24h cache).
|
45
|
-
|
46
|
-
Args:
|
47
|
-
currency_code_or_instance: Currency code (e.g., 'BTC') or Currency instance
|
48
|
-
force_refresh: If True, skip cache and fetch fresh rate
|
49
|
-
|
50
|
-
Returns:
|
51
|
-
float: 1 CURRENCY = X USD
|
52
|
-
"""
|
53
|
-
try:
|
54
|
-
# Handle both Currency instance and string code
|
55
|
-
if hasattr(currency_code_or_instance, 'code'):
|
56
|
-
# Currency instance passed
|
57
|
-
currency = currency_code_or_instance
|
58
|
-
currency_code = currency.code
|
59
|
-
else:
|
60
|
-
# String code passed
|
61
|
-
currency_code = str(currency_code_or_instance).upper()
|
62
|
-
currency = self.filter(code=currency_code).first()
|
63
|
-
|
64
|
-
# Return cached rate if fresh and not forcing refresh
|
65
|
-
if not force_refresh and currency and currency.usd_rate is not None and currency.rate_updated_at:
|
66
|
-
# Check if cache is still fresh (24 hours)
|
67
|
-
if timezone.now() - currency.rate_updated_at < timedelta(hours=24):
|
68
|
-
logger.debug(f"Using cached USD rate for {currency_code}: ${float(currency.usd_rate):.8f}")
|
69
|
-
return float(currency.usd_rate)
|
70
|
-
|
71
|
-
# Cache miss, expired, or forced refresh - fetch fresh rate
|
72
|
-
logger.info(f"Fetching fresh USD rate for {currency_code} (force_refresh={force_refresh})")
|
73
|
-
rate = get_exchange_rate(currency_code, 'USD')
|
74
|
-
rate_decimal = Decimal(str(rate)).quantize(Decimal('0.00000001'))
|
75
|
-
|
76
|
-
# Update cache
|
77
|
-
if currency:
|
78
|
-
currency.usd_rate = rate_decimal
|
79
|
-
currency.rate_updated_at = timezone.now()
|
80
|
-
currency.save(update_fields=['usd_rate', 'rate_updated_at'])
|
81
|
-
logger.info(f"Updated USD rate for {currency_code}: ${rate:.8f}")
|
82
|
-
else:
|
83
|
-
logger.warning(f"Currency {currency_code} not found in database for rate caching")
|
84
|
-
|
85
|
-
return round(rate, 8)
|
86
|
-
|
87
|
-
except CurrencyError as e:
|
88
|
-
logger.warning(f"Failed to get USD rate for {currency_code}: {e}")
|
89
|
-
# Return cached rate if available, even if stale
|
90
|
-
if currency and currency.usd_rate is not None:
|
91
|
-
logger.info(f"Using stale cached rate for {currency_code} due to API error")
|
92
|
-
return float(currency.usd_rate)
|
93
|
-
return 0.0
|
94
|
-
|
95
|
-
def get_tokens_per_usd(self, currency_code: str) -> float:
|
96
|
-
"""Get how many tokens you can buy for 1 USD."""
|
97
|
-
usd_rate = self.get_usd_rate(currency_code)
|
98
|
-
if usd_rate > 0:
|
99
|
-
return round(1.0 / usd_rate, 8)
|
100
|
-
return 0.0
|
101
|
-
|
102
|
-
def convert_to_usd(self, amount: float, currency_code: str) -> float:
|
103
|
-
"""Convert currency amount to USD."""
|
104
|
-
usd_rate = self.get_usd_rate(currency_code)
|
105
|
-
return round(amount * usd_rate, 2)
|
106
|
-
|
107
|
-
def convert_from_usd(self, usd_amount: float, currency_code: str) -> float:
|
108
|
-
"""Convert USD amount to target currency."""
|
109
|
-
tokens_per_usd = self.get_tokens_per_usd(currency_code)
|
110
|
-
return round(usd_amount * tokens_per_usd, 8)
|
111
|
-
|
112
|
-
def get_or_create_normalized(self, code: str, defaults: dict = None):
|
113
|
-
"""Simple get_or_create with uppercase code normalization."""
|
114
|
-
normalized_code = code.upper().strip() if code else ''
|
115
|
-
if not normalized_code:
|
116
|
-
raise ValueError(f"Empty currency code: '{code}'")
|
117
|
-
|
118
|
-
creation_defaults = defaults or {}
|
119
|
-
creation_defaults['code'] = normalized_code
|
120
|
-
|
121
|
-
return self.get_or_create(code__iexact=normalized_code, defaults=creation_defaults)
|
122
|
-
|
123
|
-
|
124
|
-
class NetworkManager(models.Manager):
|
125
|
-
"""Manager for Network model."""
|
126
|
-
|
127
|
-
def by_code(self, code: str):
|
128
|
-
"""Get network by code (case insensitive)."""
|
129
|
-
return self.filter(code__iexact=code).first()
|
130
|
-
|
131
|
-
def get_or_create_normalized(self, code: str, defaults: dict = None):
|
132
|
-
"""Get or create network with normalized code."""
|
133
|
-
normalized_code = code.lower().strip() if code else ''
|
134
|
-
if not normalized_code:
|
135
|
-
raise ValueError(f"Empty network code: '{code}'")
|
136
|
-
|
137
|
-
creation_defaults = defaults or {}
|
138
|
-
creation_defaults['code'] = normalized_code
|
139
|
-
|
140
|
-
return self.get_or_create(code__iexact=normalized_code, defaults=creation_defaults)
|
141
|
-
|
142
|
-
|
143
|
-
class ProviderCurrencyManager(models.Manager):
|
144
|
-
"""Manager for ProviderCurrency model."""
|
145
|
-
|
146
|
-
def enabled(self):
|
147
|
-
"""Get only enabled provider currencies."""
|
148
|
-
return self.filter(is_enabled=True)
|
149
|
-
|
150
|
-
def for_provider(self, provider_name: str):
|
151
|
-
"""Get currencies for specific provider."""
|
152
|
-
return self.filter(provider_name__iexact=provider_name)
|
153
|
-
|
154
|
-
def for_base_currency(self, currency_code: str):
|
155
|
-
"""Get provider currencies for base currency."""
|
156
|
-
return self.filter(base_currency__code__iexact=currency_code)
|
157
|
-
|
158
|
-
def for_network(self, network_code: str):
|
159
|
-
"""Get provider currencies for network."""
|
160
|
-
return self.filter(network__code__iexact=network_code)
|
161
|
-
|
162
|
-
def enabled_for_provider(self, provider_name: str):
|
163
|
-
"""Get enabled currencies for provider."""
|
164
|
-
return self.enabled().filter(provider_name__iexact=provider_name)
|
165
|
-
|
166
|
-
def popular(self):
|
167
|
-
"""Get popular currencies."""
|
168
|
-
return self.filter(is_popular=True)
|
169
|
-
|
170
|
-
def stable(self):
|
171
|
-
"""Get stable currencies."""
|
172
|
-
return self.filter(is_stable=True)
|
173
|
-
|
174
|
-
def get_currency_options_for_provider(self, provider_name: str):
|
175
|
-
"""
|
176
|
-
Get flat list of currency options for single select dropdown.
|
177
|
-
|
178
|
-
Returns:
|
179
|
-
List[dict]: List of currency option dictionaries
|
180
|
-
"""
|
181
|
-
provider_currencies = self.enabled_for_provider(provider_name).select_related(
|
182
|
-
'base_currency', 'network'
|
183
|
-
).order_by('is_popular', 'is_stable', 'base_currency__code', 'network__code')
|
184
|
-
|
185
|
-
options = []
|
186
|
-
for pc in provider_currencies:
|
187
|
-
# Create display name: "USDT (Ethereum)" or "BTC" for native currencies
|
188
|
-
if pc.network:
|
189
|
-
display_name = f"{pc.base_currency.code} ({pc.network.name})"
|
190
|
-
else:
|
191
|
-
display_name = pc.base_currency.code
|
192
|
-
|
193
|
-
# Get exchange rates using Currency manager
|
194
|
-
from ..models import Currency
|
195
|
-
usd_rate = Currency.objects.get_usd_rate(pc.base_currency.code)
|
196
|
-
tokens_per_usd = Currency.objects.get_tokens_per_usd(pc.base_currency.code)
|
197
|
-
|
198
|
-
option = {
|
199
|
-
'provider_currency_code': pc.provider_currency_code,
|
200
|
-
'display_name': display_name,
|
201
|
-
'base_currency_code': pc.base_currency.code,
|
202
|
-
'base_currency_name': pc.base_currency.name,
|
203
|
-
'network_code': pc.network.code if pc.network else None,
|
204
|
-
'network_name': pc.network.name if pc.network else None,
|
205
|
-
'currency_type': pc.base_currency.currency_type,
|
206
|
-
'is_popular': pc.is_popular,
|
207
|
-
'is_stable': pc.is_stable,
|
208
|
-
'available_for_payment': pc.available_for_payment,
|
209
|
-
'available_for_payout': pc.available_for_payout,
|
210
|
-
'min_amount': str(pc.min_amount) if pc.min_amount else None,
|
211
|
-
'max_amount': str(pc.max_amount) if pc.max_amount else None,
|
212
|
-
'logo_url': pc.logo_url,
|
213
|
-
# Exchange rates
|
214
|
-
'usd_rate': usd_rate,
|
215
|
-
'tokens_per_usd': tokens_per_usd
|
216
|
-
}
|
217
|
-
options.append(option)
|
218
|
-
|
219
|
-
# Sort: popular first, then stable, then alphabetically
|
220
|
-
def sort_key(option):
|
221
|
-
return (
|
222
|
-
0 if option['is_popular'] else 1, # Popular first
|
223
|
-
0 if option['is_stable'] else 1, # Then stable
|
224
|
-
option['base_currency_code'], # Then by base currency
|
225
|
-
option['network_name'] or '' # Then by network
|
226
|
-
)
|
227
|
-
|
228
|
-
options.sort(key=sort_key)
|
229
|
-
return options
|
230
|
-
|
231
|
-
def get_usd_rates_for_provider(self, provider_name: str):
|
232
|
-
"""
|
233
|
-
Get USD exchange rates for all provider currencies.
|
234
|
-
|
235
|
-
Returns:
|
236
|
-
dict: {provider_currency_code: {'rate': 0.0001, 'tokens_per_usd': 10000}}
|
237
|
-
"""
|
238
|
-
provider_currencies = self.enabled_for_provider(provider_name).select_related('base_currency')
|
239
|
-
rates = {}
|
240
|
-
|
241
|
-
for pc in provider_currencies:
|
242
|
-
try:
|
243
|
-
# Get rate: 1 BASE_CURRENCY = X USD
|
244
|
-
usd_rate = get_exchange_rate(pc.base_currency.code, 'USD')
|
245
|
-
|
246
|
-
# Calculate tokens per 1 USD
|
247
|
-
if usd_rate > 0:
|
248
|
-
tokens_per_usd = 1.0 / usd_rate
|
249
|
-
else:
|
250
|
-
tokens_per_usd = 0.0
|
251
|
-
|
252
|
-
rates[pc.provider_currency_code] = {
|
253
|
-
'usd_rate': round(usd_rate, 8),
|
254
|
-
'tokens_per_usd': round(tokens_per_usd, 2),
|
255
|
-
'base_currency': pc.base_currency.code,
|
256
|
-
'updated_at': timezone.now().isoformat()
|
257
|
-
}
|
258
|
-
|
259
|
-
except CurrencyError as e:
|
260
|
-
logger.warning(f"Failed to get rate for {pc.base_currency.code}: {e}")
|
261
|
-
rates[pc.provider_currency_code] = {
|
262
|
-
'usd_rate': 0.0,
|
263
|
-
'tokens_per_usd': 0.0,
|
264
|
-
'base_currency': pc.base_currency.code,
|
265
|
-
'error': str(e)
|
266
|
-
}
|
267
|
-
|
268
|
-
return rates
|
269
|
-
|
270
|
-
def convert_amount(self, amount: float, from_currency_code: str, to_currency: str = 'USD'):
|
271
|
-
"""
|
272
|
-
Convert amount from provider currency to target currency.
|
273
|
-
|
274
|
-
Args:
|
275
|
-
amount: Amount to convert
|
276
|
-
from_currency_code: Provider currency code (e.g., 'USDTERC20')
|
277
|
-
to_currency: Target currency (default: 'USD')
|
278
|
-
|
279
|
-
Returns:
|
280
|
-
dict: {'amount': converted_amount, 'rate': exchange_rate, 'from': base_currency}
|
281
|
-
"""
|
282
|
-
try:
|
283
|
-
# Find provider currency and get base currency
|
284
|
-
pc = self.get(provider_currency_code=from_currency_code)
|
285
|
-
base_currency = pc.base_currency.code
|
286
|
-
|
287
|
-
# Convert via base currency
|
288
|
-
converted_amount = convert_currency(amount, base_currency, to_currency)
|
289
|
-
rate = get_exchange_rate(base_currency, to_currency)
|
290
|
-
|
291
|
-
return {
|
292
|
-
'amount': round(converted_amount, 2),
|
293
|
-
'rate': round(rate, 8),
|
294
|
-
'from': base_currency,
|
295
|
-
'to': to_currency,
|
296
|
-
'original_amount': amount,
|
297
|
-
'provider_code': from_currency_code
|
298
|
-
}
|
299
|
-
|
300
|
-
except (CurrencyError, self.model.DoesNotExist) as e:
|
301
|
-
logger.error(f"Conversion failed for {from_currency_code}: {e}")
|
302
|
-
return {
|
303
|
-
'amount': 0.0,
|
304
|
-
'rate': 0.0,
|
305
|
-
'error': str(e)
|
306
|
-
}
|