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.
Files changed (125) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
  3. django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
  4. django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
  5. django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
  6. django_cfg/apps/payments/admin/__init__.py +23 -0
  7. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  8. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  9. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  10. django_cfg/apps/payments/admin/filters.py +259 -0
  11. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  12. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  13. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  14. django_cfg/apps/payments/config/__init__.py +65 -0
  15. django_cfg/apps/payments/config/module.py +70 -0
  16. django_cfg/apps/payments/config/providers.py +115 -0
  17. django_cfg/apps/payments/config/settings.py +96 -0
  18. django_cfg/apps/payments/config/utils.py +52 -0
  19. django_cfg/apps/payments/decorators.py +291 -0
  20. django_cfg/apps/payments/management/__init__.py +3 -0
  21. django_cfg/apps/payments/management/commands/README.md +178 -0
  22. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  23. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  24. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  25. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  26. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  27. django_cfg/apps/payments/middleware/api_access.py +294 -0
  28. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  29. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  30. django_cfg/apps/payments/migrations/0001_initial.py +125 -11
  31. django_cfg/apps/payments/models/__init__.py +18 -0
  32. django_cfg/apps/payments/models/api_keys.py +2 -2
  33. django_cfg/apps/payments/models/balance.py +2 -2
  34. django_cfg/apps/payments/models/base.py +16 -0
  35. django_cfg/apps/payments/models/events.py +2 -2
  36. django_cfg/apps/payments/models/payments.py +112 -2
  37. django_cfg/apps/payments/models/subscriptions.py +2 -2
  38. django_cfg/apps/payments/services/__init__.py +64 -7
  39. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  40. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  41. django_cfg/apps/payments/services/cache/base.py +30 -0
  42. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  43. django_cfg/apps/payments/services/core/__init__.py +17 -0
  44. django_cfg/apps/payments/services/core/balance_service.py +447 -0
  45. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  46. django_cfg/apps/payments/services/core/payment_service.py +576 -0
  47. django_cfg/apps/payments/services/core/subscription_service.py +614 -0
  48. django_cfg/apps/payments/services/internal_types.py +297 -0
  49. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  50. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  51. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  52. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  53. django_cfg/apps/payments/services/providers/__init__.py +22 -0
  54. django_cfg/apps/payments/services/providers/base.py +137 -0
  55. django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
  56. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  57. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  58. django_cfg/apps/payments/services/providers/registry.py +103 -0
  59. django_cfg/apps/payments/services/security/__init__.py +34 -0
  60. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  61. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  62. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  63. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  64. django_cfg/apps/payments/signals/__init__.py +13 -0
  65. django_cfg/apps/payments/signals/api_key_signals.py +160 -0
  66. django_cfg/apps/payments/signals/payment_signals.py +128 -0
  67. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  68. django_cfg/apps/payments/tasks/__init__.py +12 -0
  69. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  70. django_cfg/apps/payments/urls.py +5 -5
  71. django_cfg/apps/payments/utils/__init__.py +45 -0
  72. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  73. django_cfg/apps/payments/utils/config_utils.py +245 -0
  74. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  75. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  76. django_cfg/apps/payments/views/payment_views.py +40 -2
  77. django_cfg/apps/payments/views/webhook_views.py +266 -0
  78. django_cfg/apps/payments/viewsets.py +65 -0
  79. django_cfg/apps/support/signals.py +16 -4
  80. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  81. django_cfg/cli/README.md +2 -2
  82. django_cfg/cli/commands/create_project.py +1 -1
  83. django_cfg/cli/commands/info.py +1 -1
  84. django_cfg/cli/main.py +1 -1
  85. django_cfg/cli/utils.py +5 -5
  86. django_cfg/core/config.py +18 -4
  87. django_cfg/models/payments.py +546 -0
  88. django_cfg/models/revolution.py +1 -1
  89. django_cfg/models/tasks.py +51 -2
  90. django_cfg/modules/base.py +12 -6
  91. django_cfg/modules/django_currency/README.md +104 -269
  92. django_cfg/modules/django_currency/__init__.py +99 -41
  93. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  94. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  95. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  96. django_cfg/modules/django_currency/core/__init__.py +42 -0
  97. django_cfg/modules/django_currency/core/converter.py +169 -0
  98. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  99. django_cfg/modules/django_currency/core/models.py +54 -0
  100. django_cfg/modules/django_currency/database/__init__.py +25 -0
  101. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  102. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  103. django_cfg/modules/django_currency/utils/cache.py +92 -0
  104. django_cfg/modules/django_email.py +42 -4
  105. django_cfg/modules/django_unfold/dashboard.py +20 -0
  106. django_cfg/registry/core.py +10 -0
  107. django_cfg/template_archive/__init__.py +0 -0
  108. django_cfg/template_archive/django_sample.zip +0 -0
  109. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
  110. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
  111. django_cfg/apps/agents/examples/__init__.py +0 -3
  112. django_cfg/apps/agents/examples/simple_example.py +0 -161
  113. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  114. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  115. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  116. django_cfg/apps/payments/services/base.py +0 -68
  117. django_cfg/apps/payments/services/nowpayments.py +0 -78
  118. django_cfg/apps/payments/services/providers.py +0 -77
  119. django_cfg/apps/payments/services/redis_service.py +0 -215
  120. django_cfg/modules/django_currency/cache.py +0 -430
  121. django_cfg/modules/django_currency/converter.py +0 -324
  122. django_cfg/modules/django_currency/service.py +0 -277
  123. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  124. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  125. {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
+ )