django-cfg 1.2.29__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 -9
- 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 +600 -108
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +470 -64
- 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 +381 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
- 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 +343 -163
- django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +16 -20
- 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 +207 -67
- 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 -284
- 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 +344 -468
- django_cfg/apps/payments/services/core/subscription_service.py +425 -484
- 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 +232 -71
- django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
- django_cfg/apps/payments/services/providers/registry.py +429 -80
- 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 +211 -130
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +129 -98
- django_cfg/apps/payments/signals/subscription_signals.py +195 -143
- 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 +46 -47
- django_cfg/apps/payments/urls_admin.py +49 -0
- 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/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +75 -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 -498
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +65 -12
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
- 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 -178
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/managers/__init__.py +0 -22
- 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 -83
- django_cfg/apps/payments/managers/payment_manager.py +0 -44
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -56
- 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 -55
- 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 -297
- 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 -222
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -637
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- 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/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 -36
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/urls_templates.py +0 -52
- django_cfg/apps/payments/utils/__init__.py +0 -45
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -245
- 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 -62
- 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 -111
- 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 -312
- django_cfg/apps/payments/views/templates/base.py +0 -204
- 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 -164
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -240
- 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 -65
- django_cfg/core/integration.py +0 -160
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- 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.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,447 +1,522 @@
|
|
1
1
|
"""
|
2
|
-
Balance
|
2
|
+
Balance service for the Universal Payment System v2.0.
|
3
3
|
|
4
|
-
|
5
|
-
and balance validation with atomic operations.
|
4
|
+
Handles user balance operations and transaction management.
|
6
5
|
"""
|
7
6
|
|
8
|
-
import
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from
|
12
|
-
|
13
|
-
from django.db import transaction
|
14
|
-
from django.contrib.auth import get_user_model
|
15
|
-
from pydantic import BaseModel, Field, ValidationError
|
7
|
+
from typing import Optional, Dict, Any, List
|
8
|
+
from django.contrib.auth.models import User
|
9
|
+
from django.db import models
|
10
|
+
from django.utils import timezone
|
16
11
|
|
12
|
+
from .base import BaseService
|
13
|
+
from ..types import (
|
14
|
+
BalanceUpdateRequest, BalanceResult, TransactionData,
|
15
|
+
ServiceOperationResult, BalanceData
|
16
|
+
)
|
17
17
|
from ...models import UserBalance, Transaction
|
18
|
-
from ..internal_types import ServiceOperationResult, BalanceUpdateRequest, UserBalanceResult, TransactionInfo
|
19
|
-
|
20
|
-
User = get_user_model()
|
21
|
-
logger = logging.getLogger(__name__)
|
22
|
-
|
23
|
-
|
24
|
-
class BalanceOperation(BaseModel):
|
25
|
-
"""Type-safe balance operation request"""
|
26
|
-
user_id: int = Field(gt=0, description="User ID")
|
27
|
-
amount: Decimal = Field(gt=0, description="Operation amount")
|
28
|
-
currency_code: str = Field(default='USD', min_length=3, max_length=10, description="Currency code")
|
29
|
-
source: str = Field(min_length=1, description="Operation source")
|
30
|
-
reference_id: Optional[str] = Field(None, description="External reference ID")
|
31
|
-
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
32
|
-
|
33
|
-
|
34
|
-
class BalanceResult(BaseModel):
|
35
|
-
"""Type-safe balance operation result"""
|
36
|
-
success: bool
|
37
|
-
transaction_id: Optional[str] = None
|
38
|
-
balance_id: Optional[str] = None
|
39
|
-
old_balance: Decimal = Field(default=Decimal('0'))
|
40
|
-
new_balance: Decimal = Field(default=Decimal('0'))
|
41
|
-
error_message: Optional[str] = None
|
42
|
-
error_code: Optional[str] = None
|
43
18
|
|
44
19
|
|
45
|
-
class
|
46
|
-
"""Type-safe hold operation request"""
|
47
|
-
user_id: int = Field(gt=0, description="User ID")
|
48
|
-
amount: Decimal = Field(gt=0, description="Hold amount")
|
49
|
-
reason: str = Field(min_length=1, description="Hold reason")
|
50
|
-
expires_in_hours: int = Field(default=24, ge=1, le=168, description="Hold expiration in hours")
|
51
|
-
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
52
|
-
|
53
|
-
|
54
|
-
class BalanceService:
|
20
|
+
class BalanceService(BaseService):
|
55
21
|
"""
|
56
|
-
|
22
|
+
Balance service with business logic and validation.
|
57
23
|
|
58
|
-
Handles balance operations
|
59
|
-
with Redis caching and atomic database operations.
|
24
|
+
Handles balance operations using Pydantic validation and Django ORM managers.
|
60
25
|
"""
|
61
26
|
|
62
|
-
def
|
63
|
-
"""Initialize balance service with dependencies"""
|
64
|
-
pass
|
65
|
-
|
66
|
-
def add_funds(
|
67
|
-
self,
|
68
|
-
user: User,
|
69
|
-
amount: Decimal,
|
70
|
-
currency_code: str = 'USD',
|
71
|
-
source: str = 'manual',
|
72
|
-
reference_id: Optional[str] = None,
|
73
|
-
**kwargs
|
74
|
-
) -> BalanceResult:
|
27
|
+
def get_user_balance(self, user_id: int) -> BalanceResult:
|
75
28
|
"""
|
76
|
-
|
29
|
+
Get user balance, creating if doesn't exist.
|
77
30
|
|
78
31
|
Args:
|
79
|
-
|
80
|
-
amount: Amount to add
|
81
|
-
currency_code: Currency code (default: USD)
|
82
|
-
source: Source of funds (e.g., 'payment', 'manual')
|
83
|
-
reference_id: External reference ID
|
84
|
-
**kwargs: Additional metadata
|
32
|
+
user_id: User ID
|
85
33
|
|
86
34
|
Returns:
|
87
|
-
BalanceResult
|
35
|
+
BalanceResult: User balance information
|
88
36
|
"""
|
89
37
|
try:
|
90
|
-
|
91
|
-
operation = BalanceOperation(
|
92
|
-
user_id=user.id,
|
93
|
-
amount=amount,
|
94
|
-
currency_code=currency_code,
|
95
|
-
source=source,
|
96
|
-
reference_id=reference_id,
|
97
|
-
metadata=kwargs
|
98
|
-
)
|
38
|
+
self.logger.debug("Getting user balance", extra={'user_id': user_id})
|
99
39
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
defaults={
|
105
|
-
'amount_usd': Decimal('0'),
|
106
|
-
'reserved_usd': Decimal('0')
|
107
|
-
}
|
108
|
-
)
|
109
|
-
|
110
|
-
old_balance = balance.amount_usd
|
111
|
-
|
112
|
-
# Update balance
|
113
|
-
balance.amount_usd += float(amount)
|
114
|
-
balance.save(update_fields=['amount_usd', 'updated_at'])
|
115
|
-
|
116
|
-
# Create transaction record
|
117
|
-
transaction_record = Transaction.objects.create(
|
118
|
-
user=user,
|
119
|
-
transaction_type=Transaction.TransactionType.CREDIT,
|
120
|
-
amount_usd=float(amount),
|
121
|
-
balance_before=old_balance,
|
122
|
-
balance_after=balance.amount_usd,
|
123
|
-
description=f"Funds added: {source}",
|
124
|
-
reference_id=reference_id,
|
125
|
-
metadata=kwargs
|
126
|
-
)
|
127
|
-
|
128
|
-
|
40
|
+
# Get or create user
|
41
|
+
try:
|
42
|
+
user = User.objects.get(id=user_id)
|
43
|
+
except User.DoesNotExist:
|
129
44
|
return BalanceResult(
|
130
|
-
success=
|
131
|
-
|
132
|
-
|
133
|
-
old_balance=old_balance,
|
134
|
-
new_balance=balance.amount_usd
|
45
|
+
success=False,
|
46
|
+
message=f"User {user_id} not found",
|
47
|
+
error_code="user_not_found"
|
135
48
|
)
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
49
|
+
|
50
|
+
# Get or create balance using manager
|
51
|
+
balance = UserBalance.objects.get_or_create_for_user(user)
|
52
|
+
balance_data = BalanceData.model_validate(balance)
|
53
|
+
|
54
|
+
self._log_operation(
|
55
|
+
"get_user_balance",
|
56
|
+
True,
|
57
|
+
user_id=user_id,
|
58
|
+
balance_usd=balance.balance_usd
|
143
59
|
)
|
144
|
-
|
145
|
-
logger.error(f"Add funds failed for user {user.id}: {e}", exc_info=True)
|
60
|
+
|
146
61
|
return BalanceResult(
|
147
|
-
success=
|
148
|
-
|
149
|
-
|
62
|
+
success=True,
|
63
|
+
message="Balance retrieved successfully",
|
64
|
+
user_id=user_id,
|
65
|
+
balance_usd=balance.balance_usd,
|
66
|
+
data={'balance': balance_data.model_dump()}
|
150
67
|
)
|
68
|
+
|
69
|
+
except Exception as e:
|
70
|
+
return BalanceResult(**self._handle_exception(
|
71
|
+
"get_user_balance", e,
|
72
|
+
user_id=user_id
|
73
|
+
).model_dump())
|
151
74
|
|
152
|
-
def
|
153
|
-
self,
|
154
|
-
user: User,
|
155
|
-
amount: Decimal,
|
156
|
-
currency_code: str = 'USD',
|
157
|
-
source: str = 'usage',
|
158
|
-
reference_id: Optional[str] = None,
|
159
|
-
force: bool = False,
|
160
|
-
**kwargs
|
161
|
-
) -> BalanceResult:
|
75
|
+
def update_balance(self, request: BalanceUpdateRequest) -> BalanceResult:
|
162
76
|
"""
|
163
|
-
|
77
|
+
Update user balance with transaction record.
|
164
78
|
|
165
79
|
Args:
|
166
|
-
|
167
|
-
amount: Amount to deduct
|
168
|
-
currency_code: Currency code (default: USD)
|
169
|
-
source: Source of deduction (e.g., 'usage', 'subscription')
|
170
|
-
reference_id: External reference ID
|
171
|
-
force: Force deduction even if insufficient funds
|
172
|
-
**kwargs: Additional metadata
|
80
|
+
request: Balance update request with validation
|
173
81
|
|
174
82
|
Returns:
|
175
|
-
BalanceResult
|
83
|
+
BalanceResult: Updated balance information
|
176
84
|
"""
|
177
85
|
try:
|
178
|
-
# Validate
|
179
|
-
|
180
|
-
|
181
|
-
amount=amount,
|
182
|
-
currency_code=currency_code,
|
183
|
-
source=source,
|
184
|
-
reference_id=reference_id,
|
185
|
-
metadata=kwargs
|
186
|
-
)
|
86
|
+
# Validate request
|
87
|
+
if isinstance(request, dict):
|
88
|
+
request = BalanceUpdateRequest(**request)
|
187
89
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
# Check sufficient funds
|
204
|
-
if not force and balance.amount_usd < amount:
|
205
|
-
return BalanceResult(
|
206
|
-
success=False,
|
207
|
-
error_code='INSUFFICIENT_FUNDS',
|
208
|
-
error_message=f"Insufficient funds: available {balance.amount_usd}, required {amount}",
|
209
|
-
old_balance=old_balance,
|
210
|
-
new_balance=old_balance
|
211
|
-
)
|
212
|
-
|
213
|
-
# Update balance
|
214
|
-
balance.amount_usd -= float(amount)
|
215
|
-
balance.save(update_fields=['amount_usd', 'updated_at'])
|
216
|
-
|
217
|
-
# Create transaction record
|
218
|
-
transaction_record = Transaction.objects.create(
|
219
|
-
user=user,
|
220
|
-
transaction_type=Transaction.TransactionType.DEBIT,
|
221
|
-
amount_usd=-float(amount), # Negative for debit
|
222
|
-
balance_before=old_balance,
|
223
|
-
balance_after=balance.amount_usd,
|
224
|
-
description=f"Funds deducted: {source}",
|
225
|
-
reference_id=reference_id,
|
226
|
-
metadata=kwargs
|
90
|
+
self.logger.info("Updating user balance", extra={
|
91
|
+
'user_id': request.user_id,
|
92
|
+
'amount': request.amount,
|
93
|
+
'transaction_type': request.transaction_type
|
94
|
+
})
|
95
|
+
|
96
|
+
# Get user
|
97
|
+
try:
|
98
|
+
user = User.objects.get(id=request.user_id)
|
99
|
+
except User.DoesNotExist:
|
100
|
+
return BalanceResult(
|
101
|
+
success=False,
|
102
|
+
message=f"User {request.user_id} not found",
|
103
|
+
error_code="user_not_found"
|
227
104
|
)
|
228
|
-
|
229
|
-
|
105
|
+
|
106
|
+
# Get or create balance
|
107
|
+
balance = UserBalance.objects.get_or_create_for_user(user)
|
108
|
+
|
109
|
+
# Check for sufficient funds if subtracting
|
110
|
+
if request.amount < 0 and balance.balance_usd + request.amount < 0:
|
230
111
|
return BalanceResult(
|
231
|
-
success=
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
112
|
+
success=False,
|
113
|
+
message="Insufficient funds",
|
114
|
+
error_code="insufficient_funds",
|
115
|
+
user_id=request.user_id,
|
116
|
+
balance_usd=balance.balance_usd
|
236
117
|
)
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
118
|
+
|
119
|
+
# Update balance using manager
|
120
|
+
def update_balance_transaction():
|
121
|
+
if request.amount > 0:
|
122
|
+
transaction = balance.add_funds(
|
123
|
+
amount=request.amount,
|
124
|
+
transaction_type=request.transaction_type,
|
125
|
+
description=request.description,
|
126
|
+
payment_id=request.payment_id
|
127
|
+
)
|
128
|
+
else:
|
129
|
+
transaction = balance.subtract_funds(
|
130
|
+
amount=abs(request.amount),
|
131
|
+
transaction_type=request.transaction_type,
|
132
|
+
description=request.description,
|
133
|
+
payment_id=request.payment_id
|
134
|
+
)
|
135
|
+
return transaction
|
136
|
+
|
137
|
+
transaction = self._execute_with_transaction(update_balance_transaction)
|
138
|
+
|
139
|
+
# Refresh balance
|
140
|
+
balance.refresh_from_db()
|
141
|
+
|
142
|
+
# Convert to response data
|
143
|
+
balance_data = BalanceData.model_validate(balance)
|
144
|
+
transaction_data = TransactionData.model_validate(transaction)
|
145
|
+
|
146
|
+
self._log_operation(
|
147
|
+
"update_balance",
|
148
|
+
True,
|
149
|
+
user_id=request.user_id,
|
150
|
+
amount=request.amount,
|
151
|
+
new_balance=balance.balance_usd,
|
152
|
+
transaction_id=str(transaction.id)
|
244
153
|
)
|
245
|
-
|
246
|
-
logger.error(f"Deduct funds failed for user {user.id}: {e}", exc_info=True)
|
154
|
+
|
247
155
|
return BalanceResult(
|
248
|
-
success=
|
249
|
-
|
250
|
-
|
156
|
+
success=True,
|
157
|
+
message="Balance updated successfully",
|
158
|
+
user_id=request.user_id,
|
159
|
+
balance_usd=balance.balance_usd,
|
160
|
+
transaction_id=str(transaction.id),
|
161
|
+
transaction_amount=request.amount,
|
162
|
+
transaction_type=request.transaction_type,
|
163
|
+
data={
|
164
|
+
'balance': balance_data.model_dump(),
|
165
|
+
'transaction': transaction_data.model_dump()
|
166
|
+
}
|
251
167
|
)
|
168
|
+
|
169
|
+
except Exception as e:
|
170
|
+
return BalanceResult(**self._handle_exception(
|
171
|
+
"update_balance", e,
|
172
|
+
user_id=request.user_id if hasattr(request, 'user_id') else None
|
173
|
+
).model_dump())
|
252
174
|
|
253
|
-
def
|
254
|
-
self,
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
reference_id: Optional[str] = None,
|
261
|
-
**kwargs
|
262
|
-
) -> Dict[str, Any]:
|
175
|
+
def add_funds(
|
176
|
+
self,
|
177
|
+
user_id: int,
|
178
|
+
amount: float,
|
179
|
+
description: str = None,
|
180
|
+
payment_id: str = None
|
181
|
+
) -> BalanceResult:
|
263
182
|
"""
|
264
|
-
|
183
|
+
Add funds to user balance.
|
265
184
|
|
266
185
|
Args:
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
source: Transfer source description
|
272
|
-
reference_id: External reference ID
|
273
|
-
**kwargs: Additional metadata
|
186
|
+
user_id: User ID
|
187
|
+
amount: Amount to add (positive)
|
188
|
+
description: Transaction description
|
189
|
+
payment_id: Related payment ID
|
274
190
|
|
275
191
|
Returns:
|
276
|
-
|
192
|
+
BalanceResult: Updated balance
|
277
193
|
"""
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
reference_id=reference_id,
|
287
|
-
transfer_to_user_id=to_user.id,
|
288
|
-
**kwargs
|
289
|
-
)
|
290
|
-
|
291
|
-
if not deduct_result.success:
|
292
|
-
return BalanceResult(
|
293
|
-
success=False,
|
294
|
-
error_code=deduct_result.error_code,
|
295
|
-
error_message=deduct_result.error_message
|
296
|
-
)
|
297
|
-
|
298
|
-
# Add to destination user
|
299
|
-
add_result = self.add_funds(
|
300
|
-
user=to_user,
|
301
|
-
amount=amount,
|
302
|
-
currency_code=currency_code,
|
303
|
-
source=f"transfer_in:{source}",
|
304
|
-
reference_id=reference_id,
|
305
|
-
transfer_from_user_id=from_user.id,
|
306
|
-
**kwargs
|
307
|
-
)
|
308
|
-
|
309
|
-
if not add_result.success:
|
310
|
-
# This should rarely happen due to atomic transaction
|
311
|
-
logger.error(f"Transfer completion failed: {add_result.error_message}")
|
312
|
-
return BalanceResult(
|
313
|
-
success=False,
|
314
|
-
error_code='TRANSFER_COMPLETION_FAILED',
|
315
|
-
error_message='Transfer could not be completed'
|
316
|
-
)
|
317
|
-
|
318
|
-
return BalanceResult(
|
319
|
-
success=True,
|
320
|
-
from_transaction_id=deduct_result.transaction_id,
|
321
|
-
to_transaction_id=add_result.transaction_id,
|
322
|
-
amount_transferred=amount,
|
323
|
-
currency_code=currency_code
|
324
|
-
)
|
325
|
-
|
326
|
-
except Exception as e:
|
327
|
-
logger.error(f"Transfer failed from {from_user.id} to {to_user.id}: {e}", exc_info=True)
|
328
|
-
return BalanceResult(
|
329
|
-
success=False,
|
330
|
-
error_code='INTERNAL_ERROR',
|
331
|
-
error_message=f"Transfer failed: {str(e)}"
|
332
|
-
)
|
194
|
+
request = BalanceUpdateRequest(
|
195
|
+
user_id=user_id,
|
196
|
+
amount=abs(amount), # Ensure positive
|
197
|
+
transaction_type='deposit',
|
198
|
+
description=description,
|
199
|
+
payment_id=payment_id
|
200
|
+
)
|
201
|
+
return self.update_balance(request)
|
333
202
|
|
334
|
-
def
|
203
|
+
def subtract_funds(
|
335
204
|
self,
|
336
205
|
user_id: int,
|
337
|
-
|
338
|
-
|
206
|
+
amount: float,
|
207
|
+
description: str = None,
|
208
|
+
payment_id: str = None
|
209
|
+
) -> BalanceResult:
|
339
210
|
"""
|
340
|
-
|
211
|
+
Subtract funds from user balance.
|
341
212
|
|
342
213
|
Args:
|
343
214
|
user_id: User ID
|
344
|
-
|
215
|
+
amount: Amount to subtract (positive)
|
216
|
+
description: Transaction description
|
217
|
+
payment_id: Related payment ID
|
345
218
|
|
346
219
|
Returns:
|
347
|
-
|
220
|
+
BalanceResult: Updated balance
|
348
221
|
"""
|
349
|
-
|
350
|
-
|
351
|
-
#
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
id=str(balance.id),
|
358
|
-
user_id=user_id,
|
359
|
-
available_balance=Decimal(str(balance.amount_usd)),
|
360
|
-
total_balance=Decimal(str(balance.amount_usd + balance.reserved_usd)),
|
361
|
-
reserved_balance=Decimal(str(balance.reserved_usd)),
|
362
|
-
last_updated=balance.updated_at,
|
363
|
-
created_at=balance.created_at
|
364
|
-
)
|
365
|
-
|
366
|
-
except UserBalance.DoesNotExist:
|
367
|
-
return None
|
368
|
-
except Exception as e:
|
369
|
-
logger.error(f"Error getting balance for user {user_id}: {e}")
|
370
|
-
return None
|
222
|
+
request = BalanceUpdateRequest(
|
223
|
+
user_id=user_id,
|
224
|
+
amount=-abs(amount), # Ensure negative
|
225
|
+
transaction_type='withdrawal',
|
226
|
+
description=description,
|
227
|
+
payment_id=payment_id
|
228
|
+
)
|
229
|
+
return self.update_balance(request)
|
371
230
|
|
372
231
|
def get_user_transactions(
|
373
232
|
self,
|
374
|
-
|
375
|
-
currency_code: Optional[str] = None,
|
233
|
+
user_id: int,
|
376
234
|
transaction_type: Optional[str] = None,
|
377
235
|
limit: int = 50,
|
378
236
|
offset: int = 0
|
379
|
-
) ->
|
237
|
+
) -> ServiceOperationResult:
|
380
238
|
"""
|
381
239
|
Get user transaction history.
|
382
240
|
|
383
241
|
Args:
|
384
|
-
|
385
|
-
currency_code: Filter by currency code
|
242
|
+
user_id: User ID
|
386
243
|
transaction_type: Filter by transaction type
|
387
244
|
limit: Number of transactions to return
|
388
245
|
offset: Pagination offset
|
389
246
|
|
390
247
|
Returns:
|
391
|
-
|
248
|
+
ServiceOperationResult: Transaction list
|
392
249
|
"""
|
393
250
|
try:
|
394
|
-
|
251
|
+
self.logger.debug("Getting user transactions", extra={
|
252
|
+
'user_id': user_id,
|
253
|
+
'transaction_type': transaction_type,
|
254
|
+
'limit': limit,
|
255
|
+
'offset': offset
|
256
|
+
})
|
257
|
+
|
258
|
+
# Check user exists
|
259
|
+
if not User.objects.filter(id=user_id).exists():
|
260
|
+
return self._create_error_result(
|
261
|
+
f"User {user_id} not found",
|
262
|
+
"user_not_found"
|
263
|
+
)
|
395
264
|
|
396
|
-
|
397
|
-
|
265
|
+
# Build query
|
266
|
+
queryset = Transaction.objects.filter(user_id=user_id)
|
398
267
|
|
399
268
|
if transaction_type:
|
400
269
|
queryset = queryset.filter(transaction_type=transaction_type)
|
401
270
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
reference_id=txn.reference_id,
|
413
|
-
description=txn.description,
|
414
|
-
created_at=txn.created_at
|
415
|
-
)
|
416
|
-
for txn in transactions
|
271
|
+
# Get total count
|
272
|
+
total_count = queryset.count()
|
273
|
+
|
274
|
+
# Get transactions with pagination
|
275
|
+
transactions = queryset.order_by('-created_at')[offset:offset + limit]
|
276
|
+
|
277
|
+
# Convert to data
|
278
|
+
transaction_data = [
|
279
|
+
TransactionData.model_validate(transaction).model_dump()
|
280
|
+
for transaction in transactions
|
417
281
|
]
|
418
282
|
|
283
|
+
return self._create_success_result(
|
284
|
+
f"Retrieved {len(transaction_data)} transactions",
|
285
|
+
{
|
286
|
+
'transactions': transaction_data,
|
287
|
+
'total_count': total_count,
|
288
|
+
'limit': limit,
|
289
|
+
'offset': offset,
|
290
|
+
'has_more': offset + limit < total_count
|
291
|
+
}
|
292
|
+
)
|
293
|
+
|
419
294
|
except Exception as e:
|
420
|
-
|
421
|
-
|
295
|
+
return self._handle_exception(
|
296
|
+
"get_user_transactions", e,
|
297
|
+
user_id=user_id
|
298
|
+
)
|
422
299
|
|
300
|
+
def get_balance_stats(self, days: int = 30) -> ServiceOperationResult:
|
301
|
+
"""
|
302
|
+
Get balance and transaction statistics.
|
303
|
+
|
304
|
+
Args:
|
305
|
+
days: Number of days to analyze
|
306
|
+
|
307
|
+
Returns:
|
308
|
+
ServiceOperationResult: Balance statistics
|
309
|
+
"""
|
310
|
+
try:
|
311
|
+
from datetime import timedelta
|
312
|
+
|
313
|
+
since = timezone.now() - timedelta(days=days)
|
314
|
+
|
315
|
+
# Balance stats
|
316
|
+
balance_stats = UserBalance.objects.aggregate(
|
317
|
+
total_users=models.Count('user_id'),
|
318
|
+
total_balance=models.Sum('balance_usd'),
|
319
|
+
avg_balance=models.Avg('balance_usd'),
|
320
|
+
max_balance=models.Max('balance_usd'),
|
321
|
+
users_with_balance=models.Count(
|
322
|
+
'user_id',
|
323
|
+
filter=models.Q(balance_usd__gt=0)
|
324
|
+
)
|
325
|
+
)
|
326
|
+
|
327
|
+
# Transaction stats
|
328
|
+
transaction_stats = Transaction.objects.filter(
|
329
|
+
created_at__gte=since
|
330
|
+
).aggregate(
|
331
|
+
total_transactions=models.Count('id'),
|
332
|
+
total_volume=models.Sum('amount'),
|
333
|
+
deposits=models.Sum(
|
334
|
+
'amount',
|
335
|
+
filter=models.Q(transaction_type='deposit')
|
336
|
+
),
|
337
|
+
withdrawals=models.Sum(
|
338
|
+
'amount',
|
339
|
+
filter=models.Q(transaction_type='withdrawal')
|
340
|
+
),
|
341
|
+
avg_transaction=models.Avg('amount')
|
342
|
+
)
|
343
|
+
|
344
|
+
# Transaction type breakdown
|
345
|
+
type_breakdown = Transaction.objects.filter(
|
346
|
+
created_at__gte=since
|
347
|
+
).values('transaction_type').annotate(
|
348
|
+
count=models.Count('id'),
|
349
|
+
volume=models.Sum('amount')
|
350
|
+
).order_by('-count')
|
351
|
+
|
352
|
+
stats = {
|
353
|
+
'period_days': days,
|
354
|
+
'balance_stats': balance_stats,
|
355
|
+
'transaction_stats': transaction_stats,
|
356
|
+
'transaction_types': list(type_breakdown),
|
357
|
+
'generated_at': timezone.now().isoformat()
|
358
|
+
}
|
359
|
+
|
360
|
+
return self._create_success_result(
|
361
|
+
f"Balance statistics for {days} days",
|
362
|
+
stats
|
363
|
+
)
|
364
|
+
|
365
|
+
except Exception as e:
|
366
|
+
return self._handle_exception("get_balance_stats", e)
|
423
367
|
|
424
|
-
|
425
|
-
|
426
|
-
|
368
|
+
def transfer_funds(
|
369
|
+
self,
|
370
|
+
from_user_id: int,
|
371
|
+
to_user_id: int,
|
372
|
+
amount: float,
|
373
|
+
description: str = None
|
374
|
+
) -> ServiceOperationResult:
|
375
|
+
"""
|
376
|
+
Transfer funds between users.
|
427
377
|
|
428
|
-
|
429
|
-
|
430
|
-
user
|
431
|
-
amount
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
378
|
+
Args:
|
379
|
+
from_user_id: Source user ID
|
380
|
+
to_user_id: Destination user ID
|
381
|
+
amount: Amount to transfer
|
382
|
+
description: Transfer description
|
383
|
+
|
384
|
+
Returns:
|
385
|
+
ServiceOperationResult: Transfer result
|
386
|
+
"""
|
387
|
+
try:
|
388
|
+
if amount <= 0:
|
389
|
+
return self._create_error_result(
|
390
|
+
"Transfer amount must be positive",
|
391
|
+
"invalid_amount"
|
392
|
+
)
|
393
|
+
|
394
|
+
if from_user_id == to_user_id:
|
395
|
+
return self._create_error_result(
|
396
|
+
"Cannot transfer to same user",
|
397
|
+
"same_user_transfer"
|
398
|
+
)
|
399
|
+
|
400
|
+
self.logger.info("Transferring funds", extra={
|
401
|
+
'from_user_id': from_user_id,
|
402
|
+
'to_user_id': to_user_id,
|
403
|
+
'amount': amount
|
404
|
+
})
|
405
|
+
|
406
|
+
# Check both users exist
|
407
|
+
if not User.objects.filter(id=from_user_id).exists():
|
408
|
+
return self._create_error_result(
|
409
|
+
f"Source user {from_user_id} not found",
|
410
|
+
"source_user_not_found"
|
411
|
+
)
|
412
|
+
|
413
|
+
if not User.objects.filter(id=to_user_id).exists():
|
414
|
+
return self._create_error_result(
|
415
|
+
f"Destination user {to_user_id} not found",
|
416
|
+
"destination_user_not_found"
|
417
|
+
)
|
418
|
+
|
419
|
+
# Execute transfer in transaction
|
420
|
+
def transfer_transaction():
|
421
|
+
# Subtract from source
|
422
|
+
subtract_result = self.subtract_funds(
|
423
|
+
from_user_id,
|
424
|
+
amount,
|
425
|
+
f"Transfer to user {to_user_id}: {description}" if description else f"Transfer to user {to_user_id}"
|
426
|
+
)
|
427
|
+
|
428
|
+
if not subtract_result.success:
|
429
|
+
raise ValueError(subtract_result.message)
|
430
|
+
|
431
|
+
# Add to destination
|
432
|
+
add_result = self.add_funds(
|
433
|
+
to_user_id,
|
434
|
+
amount,
|
435
|
+
f"Transfer from user {from_user_id}: {description}" if description else f"Transfer from user {from_user_id}"
|
436
|
+
)
|
437
|
+
|
438
|
+
if not add_result.success:
|
439
|
+
raise ValueError(add_result.message)
|
440
|
+
|
441
|
+
return {
|
442
|
+
'from_transaction': subtract_result.transaction_id,
|
443
|
+
'to_transaction': add_result.transaction_id,
|
444
|
+
'from_balance': subtract_result.balance_usd,
|
445
|
+
'to_balance': add_result.balance_usd
|
446
|
+
}
|
447
|
+
|
448
|
+
result = self._execute_with_transaction(transfer_transaction)
|
449
|
+
|
450
|
+
self._log_operation(
|
451
|
+
"transfer_funds",
|
452
|
+
True,
|
453
|
+
from_user_id=from_user_id,
|
454
|
+
to_user_id=to_user_id,
|
455
|
+
amount=amount
|
456
|
+
)
|
457
|
+
|
458
|
+
return self._create_success_result(
|
459
|
+
"Funds transferred successfully",
|
460
|
+
{
|
461
|
+
'from_user_id': from_user_id,
|
462
|
+
'to_user_id': to_user_id,
|
463
|
+
'amount': amount,
|
464
|
+
'from_transaction_id': result['from_transaction'],
|
465
|
+
'to_transaction_id': result['to_transaction'],
|
466
|
+
'from_balance': result['from_balance'],
|
467
|
+
'to_balance': result['to_balance']
|
468
|
+
}
|
469
|
+
)
|
470
|
+
|
471
|
+
except Exception as e:
|
472
|
+
return self._handle_exception(
|
473
|
+
"transfer_funds", e,
|
474
|
+
from_user_id=from_user_id,
|
475
|
+
to_user_id=to_user_id,
|
476
|
+
amount=amount
|
477
|
+
)
|
436
478
|
|
437
|
-
def
|
438
|
-
"""
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
amount
|
444
|
-
reason
|
445
|
-
|
446
|
-
|
479
|
+
def freeze_balance(self, user_id: int, amount: float, reason: str) -> ServiceOperationResult:
|
480
|
+
"""
|
481
|
+
Freeze part of user balance (for future implementation).
|
482
|
+
|
483
|
+
Args:
|
484
|
+
user_id: User ID
|
485
|
+
amount: Amount to freeze
|
486
|
+
reason: Freeze reason
|
487
|
+
|
488
|
+
Returns:
|
489
|
+
ServiceOperationResult: Freeze result
|
490
|
+
"""
|
491
|
+
# Placeholder for future frozen balance functionality
|
492
|
+
return self._create_error_result(
|
493
|
+
"Balance freezing not yet implemented",
|
494
|
+
"not_implemented"
|
447
495
|
)
|
496
|
+
|
497
|
+
def health_check(self) -> ServiceOperationResult:
|
498
|
+
"""Perform balance service health check."""
|
499
|
+
try:
|
500
|
+
# Check database connectivity
|
501
|
+
balance_count = UserBalance.objects.count()
|
502
|
+
transaction_count = Transaction.objects.count()
|
503
|
+
|
504
|
+
# Check for recent activity
|
505
|
+
recent_transactions = Transaction.objects.filter(
|
506
|
+
created_at__gte=timezone.now() - timezone.timedelta(hours=1)
|
507
|
+
).count()
|
508
|
+
|
509
|
+
stats = {
|
510
|
+
'total_balances': balance_count,
|
511
|
+
'total_transactions': transaction_count,
|
512
|
+
'recent_transactions': recent_transactions,
|
513
|
+
'service_name': 'BalanceService'
|
514
|
+
}
|
515
|
+
|
516
|
+
return self._create_success_result(
|
517
|
+
"BalanceService is healthy",
|
518
|
+
stats
|
519
|
+
)
|
520
|
+
|
521
|
+
except Exception as e:
|
522
|
+
return self._handle_exception("health_check", e)
|