django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- django_cfg/apps/payments/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/config/__init__.py +65 -0
- django_cfg/apps/payments/config/module.py +70 -0
- django_cfg/apps/payments/config/providers.py +115 -0
- django_cfg/apps/payments/config/settings.py +96 -0
- django_cfg/apps/payments/config/utils.py +52 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +294 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +125 -11
- django_cfg/apps/payments/models/__init__.py +18 -0
- django_cfg/apps/payments/models/api_keys.py +2 -2
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/base.py +16 -0
- django_cfg/apps/payments/models/events.py +2 -2
- django_cfg/apps/payments/models/payments.py +112 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +64 -7
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +447 -0
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +576 -0
- django_cfg/apps/payments/services/core/subscription_service.py +614 -0
- django_cfg/apps/payments/services/internal_types.py +297 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +22 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +103 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +160 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +45 -0
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +245 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/payments/views/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +12 -6
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- django_cfg/apps/payments/services/base.py +0 -68
- django_cfg/apps/payments/services/nowpayments.py +0 -78
- django_cfg/apps/payments/services/providers.py +0 -77
- django_cfg/apps/payments/services/redis_service.py +0 -215
- django_cfg/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,447 @@
|
|
1
|
+
"""
|
2
|
+
Balance Service - Core balance management and transaction processing.
|
3
|
+
|
4
|
+
This service handles user balance operations, transaction recording,
|
5
|
+
and balance validation with atomic operations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from typing import Dict, Any, Optional, List
|
10
|
+
from decimal import Decimal
|
11
|
+
from datetime import timezone
|
12
|
+
|
13
|
+
from django.db import transaction
|
14
|
+
from django.contrib.auth import get_user_model
|
15
|
+
from pydantic import BaseModel, Field, ValidationError
|
16
|
+
|
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
|
+
|
44
|
+
|
45
|
+
class HoldOperation(BaseModel):
|
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:
|
55
|
+
"""
|
56
|
+
Universal balance management service.
|
57
|
+
|
58
|
+
Handles balance operations, transaction recording, and hold management
|
59
|
+
with Redis caching and atomic database operations.
|
60
|
+
"""
|
61
|
+
|
62
|
+
def __init__(self):
|
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:
|
75
|
+
"""
|
76
|
+
Add funds to user balance atomically.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
user: User object
|
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
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
BalanceResult with operation status
|
88
|
+
"""
|
89
|
+
try:
|
90
|
+
# Validate operation
|
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
|
+
)
|
99
|
+
|
100
|
+
with transaction.atomic():
|
101
|
+
# Get or create balance
|
102
|
+
balance, created = UserBalance.objects.get_or_create(
|
103
|
+
user=user,
|
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
|
+
|
129
|
+
return BalanceResult(
|
130
|
+
success=True,
|
131
|
+
transaction_id=str(transaction_record.id),
|
132
|
+
balance_id=str(balance.id),
|
133
|
+
old_balance=old_balance,
|
134
|
+
new_balance=balance.amount_usd
|
135
|
+
)
|
136
|
+
|
137
|
+
except ValidationError as e:
|
138
|
+
logger.error(f"Balance operation validation error: {e}")
|
139
|
+
return BalanceResult(
|
140
|
+
success=False,
|
141
|
+
error_code='VALIDATION_ERROR',
|
142
|
+
error_message=f"Invalid operation data: {e}"
|
143
|
+
)
|
144
|
+
except Exception as e:
|
145
|
+
logger.error(f"Add funds failed for user {user.id}: {e}", exc_info=True)
|
146
|
+
return BalanceResult(
|
147
|
+
success=False,
|
148
|
+
error_code='INTERNAL_ERROR',
|
149
|
+
error_message=f"Internal error: {str(e)}"
|
150
|
+
)
|
151
|
+
|
152
|
+
def deduct_funds(
|
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:
|
162
|
+
"""
|
163
|
+
Deduct funds from user balance with insufficient funds check.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
user: User object
|
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
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
BalanceResult with operation status
|
176
|
+
"""
|
177
|
+
try:
|
178
|
+
# Validate operation
|
179
|
+
operation = BalanceOperation(
|
180
|
+
user_id=user.id,
|
181
|
+
amount=amount,
|
182
|
+
currency_code=currency_code,
|
183
|
+
source=source,
|
184
|
+
reference_id=reference_id,
|
185
|
+
metadata=kwargs
|
186
|
+
)
|
187
|
+
|
188
|
+
with transaction.atomic():
|
189
|
+
# Get balance
|
190
|
+
try:
|
191
|
+
balance = UserBalance.objects.get(
|
192
|
+
user=user
|
193
|
+
)
|
194
|
+
except UserBalance.DoesNotExist:
|
195
|
+
return BalanceResult(
|
196
|
+
success=False,
|
197
|
+
error_code='BALANCE_NOT_FOUND',
|
198
|
+
error_message=f"No balance found for currency {currency_code}"
|
199
|
+
)
|
200
|
+
|
201
|
+
old_balance = balance.amount_usd
|
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
|
227
|
+
)
|
228
|
+
|
229
|
+
|
230
|
+
return BalanceResult(
|
231
|
+
success=True,
|
232
|
+
transaction_id=str(transaction_record.id),
|
233
|
+
balance_id=str(balance.id),
|
234
|
+
old_balance=old_balance,
|
235
|
+
new_balance=balance.amount_usd
|
236
|
+
)
|
237
|
+
|
238
|
+
except ValidationError as e:
|
239
|
+
logger.error(f"Balance operation validation error: {e}")
|
240
|
+
return BalanceResult(
|
241
|
+
success=False,
|
242
|
+
error_code='VALIDATION_ERROR',
|
243
|
+
error_message=f"Invalid operation data: {e}"
|
244
|
+
)
|
245
|
+
except Exception as e:
|
246
|
+
logger.error(f"Deduct funds failed for user {user.id}: {e}", exc_info=True)
|
247
|
+
return BalanceResult(
|
248
|
+
success=False,
|
249
|
+
error_code='INTERNAL_ERROR',
|
250
|
+
error_message=f"Internal error: {str(e)}"
|
251
|
+
)
|
252
|
+
|
253
|
+
def transfer_funds(
|
254
|
+
self,
|
255
|
+
from_user: User,
|
256
|
+
to_user: User,
|
257
|
+
amount: Decimal,
|
258
|
+
currency_code: str = 'USD',
|
259
|
+
source: str = 'transfer',
|
260
|
+
reference_id: Optional[str] = None,
|
261
|
+
**kwargs
|
262
|
+
) -> Dict[str, Any]:
|
263
|
+
"""
|
264
|
+
Transfer funds between users atomically.
|
265
|
+
|
266
|
+
Args:
|
267
|
+
from_user: Source user
|
268
|
+
to_user: Destination user
|
269
|
+
amount: Amount to transfer
|
270
|
+
currency_code: Currency code (default: USD)
|
271
|
+
source: Transfer source description
|
272
|
+
reference_id: External reference ID
|
273
|
+
**kwargs: Additional metadata
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
Transfer result with both transaction IDs
|
277
|
+
"""
|
278
|
+
try:
|
279
|
+
with transaction.atomic():
|
280
|
+
# Deduct from source user
|
281
|
+
deduct_result = self.deduct_funds(
|
282
|
+
user=from_user,
|
283
|
+
amount=amount,
|
284
|
+
currency_code=currency_code,
|
285
|
+
source=f"transfer_out:{source}",
|
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
|
+
)
|
333
|
+
|
334
|
+
def get_user_balance(
|
335
|
+
self,
|
336
|
+
user_id: int,
|
337
|
+
currency_code: str = 'USD'
|
338
|
+
) -> Optional['UserBalanceResult']:
|
339
|
+
"""
|
340
|
+
Get user balance.
|
341
|
+
|
342
|
+
Args:
|
343
|
+
user_id: User ID
|
344
|
+
currency_code: Currency code (default: USD)
|
345
|
+
|
346
|
+
Returns:
|
347
|
+
Balance information or None if not found
|
348
|
+
"""
|
349
|
+
try:
|
350
|
+
|
351
|
+
# Get from database
|
352
|
+
balance = UserBalance.objects.get(
|
353
|
+
user_id=user_id
|
354
|
+
)
|
355
|
+
|
356
|
+
return UserBalanceResult(
|
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
|
371
|
+
|
372
|
+
def get_user_transactions(
|
373
|
+
self,
|
374
|
+
user: User,
|
375
|
+
currency_code: Optional[str] = None,
|
376
|
+
transaction_type: Optional[str] = None,
|
377
|
+
limit: int = 50,
|
378
|
+
offset: int = 0
|
379
|
+
) -> List[TransactionInfo]:
|
380
|
+
"""
|
381
|
+
Get user transaction history.
|
382
|
+
|
383
|
+
Args:
|
384
|
+
user: User object
|
385
|
+
currency_code: Filter by currency code
|
386
|
+
transaction_type: Filter by transaction type
|
387
|
+
limit: Number of transactions to return
|
388
|
+
offset: Pagination offset
|
389
|
+
|
390
|
+
Returns:
|
391
|
+
List of TransactionInfo objects
|
392
|
+
"""
|
393
|
+
try:
|
394
|
+
queryset = Transaction.objects.filter(user=user)
|
395
|
+
|
396
|
+
if currency_code:
|
397
|
+
queryset = queryset.filter(currency_code=currency_code)
|
398
|
+
|
399
|
+
if transaction_type:
|
400
|
+
queryset = queryset.filter(transaction_type=transaction_type)
|
401
|
+
|
402
|
+
transactions = queryset.order_by('-created_at')[offset:offset+limit]
|
403
|
+
|
404
|
+
return [
|
405
|
+
TransactionInfo(
|
406
|
+
id=str(txn.id),
|
407
|
+
user_id=txn.user.id,
|
408
|
+
transaction_type=txn.transaction_type,
|
409
|
+
amount=txn.amount,
|
410
|
+
balance_after=txn.balance_after,
|
411
|
+
source=txn.source,
|
412
|
+
reference_id=txn.reference_id,
|
413
|
+
description=txn.description,
|
414
|
+
created_at=txn.created_at
|
415
|
+
)
|
416
|
+
for txn in transactions
|
417
|
+
]
|
418
|
+
|
419
|
+
except Exception as e:
|
420
|
+
logger.error(f"Error getting transactions for user {user.id}: {e}")
|
421
|
+
return []
|
422
|
+
|
423
|
+
|
424
|
+
# Alias methods for backward compatibility with tests
|
425
|
+
def credit_balance(self, request: 'BalanceUpdateRequest') -> 'ServiceOperationResult':
|
426
|
+
"""Alias for add_funds method."""
|
427
|
+
|
428
|
+
user = User.objects.get(id=request.user_id)
|
429
|
+
return self.add_funds(
|
430
|
+
user=user,
|
431
|
+
amount=request.amount,
|
432
|
+
source=request.source,
|
433
|
+
reference_id=request.reference_id,
|
434
|
+
description=getattr(request, 'description', None)
|
435
|
+
)
|
436
|
+
|
437
|
+
def debit_balance(self, request: 'BalanceUpdateRequest') -> 'ServiceOperationResult':
|
438
|
+
"""Alias for deduct_funds method."""
|
439
|
+
|
440
|
+
user = User.objects.get(id=request.user_id)
|
441
|
+
return self.deduct_funds(
|
442
|
+
user=user,
|
443
|
+
amount=request.amount,
|
444
|
+
reason=request.source,
|
445
|
+
reference_id=request.reference_id,
|
446
|
+
description=getattr(request, 'description', None)
|
447
|
+
)
|