django-cfg 1.2.22__py3-none-any.whl → 1.2.23__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.
Files changed (67) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin/__init__.py +23 -0
  3. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  4. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  5. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  6. django_cfg/apps/payments/admin/filters.py +259 -0
  7. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  8. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  9. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  10. django_cfg/apps/payments/config/__init__.py +87 -0
  11. django_cfg/apps/payments/config/module.py +162 -0
  12. django_cfg/apps/payments/config/providers.py +93 -0
  13. django_cfg/apps/payments/config/settings.py +136 -0
  14. django_cfg/apps/payments/config/utils.py +198 -0
  15. django_cfg/apps/payments/decorators.py +291 -0
  16. django_cfg/apps/payments/middleware/api_access.py +261 -0
  17. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  18. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  19. django_cfg/apps/payments/migrations/0001_initial.py +32 -11
  20. django_cfg/apps/payments/models/__init__.py +18 -0
  21. django_cfg/apps/payments/models/api_keys.py +2 -2
  22. django_cfg/apps/payments/models/balance.py +2 -2
  23. django_cfg/apps/payments/models/base.py +16 -0
  24. django_cfg/apps/payments/models/events.py +2 -2
  25. django_cfg/apps/payments/models/payments.py +2 -2
  26. django_cfg/apps/payments/models/subscriptions.py +2 -2
  27. django_cfg/apps/payments/services/__init__.py +58 -7
  28. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  29. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  30. django_cfg/apps/payments/services/cache/base.py +30 -0
  31. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  32. django_cfg/apps/payments/services/core/__init__.py +17 -0
  33. django_cfg/apps/payments/services/core/balance_service.py +449 -0
  34. django_cfg/apps/payments/services/core/payment_service.py +393 -0
  35. django_cfg/apps/payments/services/core/subscription_service.py +616 -0
  36. django_cfg/apps/payments/services/internal_types.py +266 -0
  37. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  38. django_cfg/apps/payments/services/providers/__init__.py +19 -0
  39. django_cfg/apps/payments/services/providers/base.py +137 -0
  40. django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
  41. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  42. django_cfg/apps/payments/services/providers/registry.py +99 -0
  43. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  44. django_cfg/apps/payments/signals/__init__.py +13 -0
  45. django_cfg/apps/payments/signals/api_key_signals.py +150 -0
  46. django_cfg/apps/payments/signals/payment_signals.py +127 -0
  47. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  48. django_cfg/apps/payments/urls.py +5 -5
  49. django_cfg/apps/payments/utils/__init__.py +42 -0
  50. django_cfg/apps/payments/utils/config_utils.py +243 -0
  51. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  52. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  53. django_cfg/apps/support/signals.py +16 -4
  54. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  55. django_cfg/models/revolution.py +1 -1
  56. django_cfg/modules/base.py +1 -1
  57. django_cfg/modules/django_email.py +42 -4
  58. django_cfg/modules/django_unfold/dashboard.py +20 -0
  59. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
  60. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/RECORD +63 -26
  61. django_cfg/apps/payments/services/base.py +0 -68
  62. django_cfg/apps/payments/services/nowpayments.py +0 -78
  63. django_cfg/apps/payments/services/providers.py +0 -77
  64. django_cfg/apps/payments/services/redis_service.py +0 -215
  65. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
  66. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
  67. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,135 @@
1
+ """
2
+ Simple cache implementation for API keys and rate limiting.
3
+ ONLY for API access control - NOT payment data!
4
+ """
5
+
6
+ import logging
7
+ from typing import Optional, Any
8
+ from django.core.cache import cache
9
+
10
+ from .base import CacheInterface
11
+ from ...utils.config_utils import CacheConfigHelper
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class SimpleCache(CacheInterface):
17
+ """
18
+ Simple cache implementation using Django's cache framework.
19
+
20
+ Falls back gracefully when cache is unavailable.
21
+ """
22
+
23
+ def __init__(self, prefix: str = "payments"):
24
+ self.prefix = prefix
25
+ # Use config helper to check if cache is enabled
26
+ self.enabled = CacheConfigHelper.is_cache_enabled()
27
+
28
+ def _make_key(self, key: str) -> str:
29
+ """Create prefixed cache key."""
30
+ return f"{self.prefix}:{key}"
31
+
32
+ def get(self, key: str) -> Optional[Any]:
33
+ """Get value from cache."""
34
+ if not self.enabled:
35
+ return None
36
+
37
+ try:
38
+ cache_key = self._make_key(key)
39
+ return cache.get(cache_key)
40
+ except Exception as e:
41
+ logger.warning(f"Cache get failed for key {key}: {e}")
42
+ return None
43
+
44
+ def set(self, key: str, value: Any, timeout: Optional[int] = None) -> bool:
45
+ """Set value in cache."""
46
+ if not self.enabled:
47
+ return False
48
+
49
+ try:
50
+ cache_key = self._make_key(key)
51
+ cache.set(cache_key, value, timeout)
52
+ return True
53
+ except Exception as e:
54
+ logger.warning(f"Cache set failed for key {key}: {e}")
55
+ return False
56
+
57
+ def delete(self, key: str) -> bool:
58
+ """Delete value from cache."""
59
+ if not self.enabled:
60
+ return False
61
+
62
+ try:
63
+ cache_key = self._make_key(key)
64
+ cache.delete(cache_key)
65
+ return True
66
+ except Exception as e:
67
+ logger.warning(f"Cache delete failed for key {key}: {e}")
68
+ return False
69
+
70
+ def exists(self, key: str) -> bool:
71
+ """Check if key exists in cache."""
72
+ if not self.enabled:
73
+ return False
74
+
75
+ try:
76
+ cache_key = self._make_key(key)
77
+ return cache.get(cache_key) is not None
78
+ except Exception as e:
79
+ logger.warning(f"Cache exists check failed for key {key}: {e}")
80
+ return False
81
+
82
+
83
+ class ApiKeyCache:
84
+ """Specialized cache for API key operations."""
85
+
86
+ def __init__(self):
87
+ self.cache = SimpleCache("api_keys")
88
+ # Get timeout from config
89
+ self.default_timeout = CacheConfigHelper.get_cache_timeout('api_key')
90
+
91
+ def get_api_key_data(self, api_key: str) -> Optional[dict]:
92
+ """Get cached API key data."""
93
+ return self.cache.get(f"key:{api_key}")
94
+
95
+ def cache_api_key_data(self, api_key: str, data: dict) -> bool:
96
+ """Cache API key data."""
97
+ return self.cache.set(f"key:{api_key}", data, self.default_timeout)
98
+
99
+ def invalidate_api_key(self, api_key: str) -> bool:
100
+ """Invalidate cached API key."""
101
+ return self.cache.delete(f"key:{api_key}")
102
+
103
+
104
+ class RateLimitCache:
105
+ """Specialized cache for rate limiting."""
106
+
107
+ def __init__(self):
108
+ self.cache = SimpleCache("rate_limit")
109
+
110
+ def get_usage_count(self, user_id: int, endpoint_group: str, window: str = "hour") -> int:
111
+ """Get current usage count for rate limiting."""
112
+ key = f"usage:{user_id}:{endpoint_group}:{window}"
113
+ count = self.cache.get(key)
114
+ return count if count is not None else 0
115
+
116
+ def increment_usage(self, user_id: int, endpoint_group: str, window: str = "hour", ttl: Optional[int] = None) -> int:
117
+ """Increment usage count and return new count."""
118
+ key = f"usage:{user_id}:{endpoint_group}:{window}"
119
+
120
+ # Get current count
121
+ current = self.get_usage_count(user_id, endpoint_group, window)
122
+ new_count = current + 1
123
+
124
+ # Use config helper for TTL if not provided
125
+ if ttl is None:
126
+ ttl = CacheConfigHelper.get_cache_timeout('rate_limit')
127
+
128
+ # Set new count with TTL
129
+ self.cache.set(key, new_count, ttl)
130
+ return new_count
131
+
132
+ def reset_usage(self, user_id: int, endpoint_group: str, window: str = "hour") -> bool:
133
+ """Reset usage count."""
134
+ key = f"usage:{user_id}:{endpoint_group}:{window}"
135
+ return self.cache.delete(key)
@@ -0,0 +1,17 @@
1
+ """
2
+ Core payment services.
3
+
4
+ Main business logic services for the payment system.
5
+ """
6
+
7
+ from .payment_service import PaymentService
8
+ from .balance_service import BalanceService
9
+ from .subscription_service import SubscriptionService
10
+ # Core services only - no legacy adapters
11
+
12
+ __all__ = [
13
+ 'PaymentService',
14
+ 'BalanceService',
15
+ 'SubscriptionService',
16
+ # No legacy services
17
+ ]
@@ -0,0 +1,449 @@
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
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[Dict[str, Any]]:
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 transaction dictionaries
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
+ {
406
+ 'id': str(txn.id),
407
+ 'transaction_type': txn.transaction_type,
408
+ 'amount': str(txn.amount),
409
+ 'currency_code': txn.currency_code,
410
+ 'balance_before': str(txn.balance_before),
411
+ 'balance_after': str(txn.balance_after),
412
+ 'source': txn.source,
413
+ 'reference_id': txn.reference_id,
414
+ 'description': txn.description,
415
+ 'created_at': txn.created_at.isoformat(),
416
+ 'metadata': txn.metadata
417
+ }
418
+ for txn in transactions
419
+ ]
420
+
421
+ except Exception as e:
422
+ logger.error(f"Error getting transactions for user {user.id}: {e}")
423
+ return []
424
+
425
+
426
+ # Alias methods for backward compatibility with tests
427
+ def credit_balance(self, request: 'BalanceUpdateRequest') -> 'ServiceOperationResult':
428
+ """Alias for add_funds method."""
429
+
430
+ user = User.objects.get(id=request.user_id)
431
+ return self.add_funds(
432
+ user=user,
433
+ amount=request.amount,
434
+ source=request.source,
435
+ reference_id=request.reference_id,
436
+ description=getattr(request, 'description', None)
437
+ )
438
+
439
+ def debit_balance(self, request: 'BalanceUpdateRequest') -> 'ServiceOperationResult':
440
+ """Alias for deduct_funds method."""
441
+
442
+ user = User.objects.get(id=request.user_id)
443
+ return self.deduct_funds(
444
+ user=user,
445
+ amount=request.amount,
446
+ reason=request.source,
447
+ reference_id=request.reference_id,
448
+ description=getattr(request, 'description', None)
449
+ )