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.
Files changed (258) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/health/views.py +4 -2
  3. django_cfg/apps/knowbase/config/settings.py +16 -15
  4. django_cfg/apps/payments/README.md +326 -0
  5. django_cfg/apps/payments/admin/__init__.py +20 -9
  6. django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
  7. django_cfg/apps/payments/admin/balance_admin.py +592 -297
  8. django_cfg/apps/payments/admin/currencies_admin.py +600 -108
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +470 -64
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
  39. django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
  43. django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +16 -20
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +207 -67
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -284
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +344 -468
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -484
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +232 -71
  74. django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
  75. django_cfg/apps/payments/services/providers/registry.py +429 -80
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +211 -130
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +129 -98
  85. django_cfg/apps/payments/signals/subscription_signals.py +195 -143
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +46 -47
  93. django_cfg/apps/payments/urls_admin.py +49 -0
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/apps/tasks/urls.py +0 -2
  110. django_cfg/apps/tasks/urls_admin.py +14 -0
  111. django_cfg/apps/urls.py +4 -4
  112. django_cfg/config.py +1 -1
  113. django_cfg/core/config.py +75 -4
  114. django_cfg/core/generation.py +25 -4
  115. django_cfg/core/integration/README.md +363 -0
  116. django_cfg/core/integration/__init__.py +47 -0
  117. django_cfg/core/integration/commands_collector.py +239 -0
  118. django_cfg/core/integration/display/__init__.py +15 -0
  119. django_cfg/core/integration/display/base.py +157 -0
  120. django_cfg/core/integration/display/ngrok.py +164 -0
  121. django_cfg/core/integration/display/startup.py +815 -0
  122. django_cfg/core/integration/url_integration.py +123 -0
  123. django_cfg/core/integration/version_checker.py +160 -0
  124. django_cfg/management/commands/auto_generate.py +4 -0
  125. django_cfg/management/commands/check_settings.py +6 -0
  126. django_cfg/management/commands/clear_constance.py +5 -2
  127. django_cfg/management/commands/create_token.py +6 -0
  128. django_cfg/management/commands/list_urls.py +6 -0
  129. django_cfg/management/commands/migrate_all.py +6 -0
  130. django_cfg/management/commands/migrator.py +3 -0
  131. django_cfg/management/commands/rundramatiq.py +6 -0
  132. django_cfg/management/commands/runserver_ngrok.py +51 -29
  133. django_cfg/management/commands/script.py +6 -0
  134. django_cfg/management/commands/show_config.py +12 -2
  135. django_cfg/management/commands/show_urls.py +4 -0
  136. django_cfg/management/commands/superuser.py +6 -0
  137. django_cfg/management/commands/task_clear.py +4 -1
  138. django_cfg/management/commands/task_status.py +3 -1
  139. django_cfg/management/commands/test_email.py +3 -0
  140. django_cfg/management/commands/test_telegram.py +6 -0
  141. django_cfg/management/commands/test_twilio.py +6 -0
  142. django_cfg/management/commands/tree.py +6 -0
  143. django_cfg/management/commands/validate_config.py +155 -149
  144. django_cfg/models/constance.py +31 -11
  145. django_cfg/models/payments.py +175 -498
  146. django_cfg/modules/django_currency/__init__.py +16 -11
  147. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  148. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  149. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  150. django_cfg/modules/django_currency/core/__init__.py +1 -7
  151. django_cfg/modules/django_currency/core/converter.py +18 -23
  152. django_cfg/modules/django_currency/core/models.py +122 -11
  153. django_cfg/modules/django_currency/database/__init__.py +4 -4
  154. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  155. django_cfg/modules/django_logger.py +160 -146
  156. django_cfg/modules/django_unfold/dashboard.py +65 -12
  157. django_cfg/registry/core.py +1 -0
  158. django_cfg/template_archive/django_sample.zip +0 -0
  159. django_cfg/templates/admin/components/action_grid.html +9 -9
  160. django_cfg/templates/admin/components/metric_card.html +5 -5
  161. django_cfg/templates/admin/components/status_badge.html +2 -2
  162. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  163. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  164. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  165. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  166. django_cfg/utils/smart_defaults.py +222 -571
  167. django_cfg/utils/toolkit.py +51 -11
  168. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
  169. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
  170. django_cfg/apps/payments/__init__.py +0 -8
  171. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  172. django_cfg/apps/payments/config/module.py +0 -70
  173. django_cfg/apps/payments/config/providers.py +0 -105
  174. django_cfg/apps/payments/config/settings.py +0 -96
  175. django_cfg/apps/payments/config/utils.py +0 -52
  176. django_cfg/apps/payments/decorators.py +0 -291
  177. django_cfg/apps/payments/management/commands/README.md +0 -178
  178. django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
  179. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  180. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  181. django_cfg/apps/payments/managers/__init__.py +0 -22
  182. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  183. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  184. django_cfg/apps/payments/managers/currency_manager.py +0 -83
  185. django_cfg/apps/payments/managers/payment_manager.py +0 -44
  186. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  187. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  188. django_cfg/apps/payments/models/events.py +0 -73
  189. django_cfg/apps/payments/serializers/__init__.py +0 -56
  190. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  191. django_cfg/apps/payments/serializers/balance.py +0 -59
  192. django_cfg/apps/payments/serializers/currencies.py +0 -55
  193. django_cfg/apps/payments/serializers/payments.py +0 -62
  194. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  195. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  196. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  197. django_cfg/apps/payments/services/cache/base.py +0 -30
  198. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  199. django_cfg/apps/payments/services/internal_types.py +0 -297
  200. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  201. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  202. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
  203. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  204. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  205. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  206. django_cfg/apps/payments/services/security/__init__.py +0 -34
  207. django_cfg/apps/payments/services/security/error_handler.py +0 -637
  208. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  209. django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
  210. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  211. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  212. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  213. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  214. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  215. django_cfg/apps/payments/tasks/__init__.py +0 -12
  216. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  217. django_cfg/apps/payments/templates/payments/base.html +0 -182
  218. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  219. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  220. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
  221. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  222. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
  223. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
  224. django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
  225. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  226. django_cfg/apps/payments/urls_templates.py +0 -52
  227. django_cfg/apps/payments/utils/__init__.py +0 -45
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -245
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -62
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -111
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -312
  241. django_cfg/apps/payments/views/templates/base.py +0 -204
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -164
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -240
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -65
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  252. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  253. django_cfg/template_archive/.gitignore +0 -1
  254. django_cfg/template_archive/__init__.py +0 -0
  255. django_cfg/urls.py +0 -33
  256. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  257. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  258. {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 Service - Core balance management and transaction processing.
2
+ Balance service for the Universal Payment System v2.0.
3
3
 
4
- This service handles user balance operations, transaction recording,
5
- and balance validation with atomic operations.
4
+ Handles user balance operations and transaction management.
6
5
  """
7
6
 
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
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 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:
20
+ class BalanceService(BaseService):
55
21
  """
56
- Universal balance management service.
22
+ Balance service with business logic and validation.
57
23
 
58
- Handles balance operations, transaction recording, and hold management
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 __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:
27
+ def get_user_balance(self, user_id: int) -> BalanceResult:
75
28
  """
76
- Add funds to user balance atomically.
29
+ Get user balance, creating if doesn't exist.
77
30
 
78
31
  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
32
+ user_id: User ID
85
33
 
86
34
  Returns:
87
- BalanceResult with operation status
35
+ BalanceResult: User balance information
88
36
  """
89
37
  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
- )
38
+ self.logger.debug("Getting user balance", extra={'user_id': user_id})
99
39
 
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
-
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=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
45
+ success=False,
46
+ message=f"User {user_id} not found",
47
+ error_code="user_not_found"
135
48
  )
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}"
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
- except Exception as e:
145
- logger.error(f"Add funds failed for user {user.id}: {e}", exc_info=True)
60
+
146
61
  return BalanceResult(
147
- success=False,
148
- error_code='INTERNAL_ERROR',
149
- error_message=f"Internal error: {str(e)}"
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 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:
75
+ def update_balance(self, request: BalanceUpdateRequest) -> BalanceResult:
162
76
  """
163
- Deduct funds from user balance with insufficient funds check.
77
+ Update user balance with transaction record.
164
78
 
165
79
  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
80
+ request: Balance update request with validation
173
81
 
174
82
  Returns:
175
- BalanceResult with operation status
83
+ BalanceResult: Updated balance information
176
84
  """
177
85
  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
- )
86
+ # Validate request
87
+ if isinstance(request, dict):
88
+ request = BalanceUpdateRequest(**request)
187
89
 
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
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=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
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
- 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}"
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
- except Exception as e:
246
- logger.error(f"Deduct funds failed for user {user.id}: {e}", exc_info=True)
154
+
247
155
  return BalanceResult(
248
- success=False,
249
- error_code='INTERNAL_ERROR',
250
- error_message=f"Internal error: {str(e)}"
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 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]:
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
- Transfer funds between users atomically.
183
+ Add funds to user balance.
265
184
 
266
185
  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
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
- Transfer result with both transaction IDs
192
+ BalanceResult: Updated balance
277
193
  """
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
- )
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 get_user_balance(
203
+ def subtract_funds(
335
204
  self,
336
205
  user_id: int,
337
- currency_code: str = 'USD'
338
- ) -> Optional['UserBalanceResult']:
206
+ amount: float,
207
+ description: str = None,
208
+ payment_id: str = None
209
+ ) -> BalanceResult:
339
210
  """
340
- Get user balance.
211
+ Subtract funds from user balance.
341
212
 
342
213
  Args:
343
214
  user_id: User ID
344
- currency_code: Currency code (default: USD)
215
+ amount: Amount to subtract (positive)
216
+ description: Transaction description
217
+ payment_id: Related payment ID
345
218
 
346
219
  Returns:
347
- Balance information or None if not found
220
+ BalanceResult: Updated balance
348
221
  """
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
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
- user: User,
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
- ) -> List[TransactionInfo]:
237
+ ) -> ServiceOperationResult:
380
238
  """
381
239
  Get user transaction history.
382
240
 
383
241
  Args:
384
- user: User object
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
- List of TransactionInfo objects
248
+ ServiceOperationResult: Transaction list
392
249
  """
393
250
  try:
394
- queryset = Transaction.objects.filter(user=user)
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
- if currency_code:
397
- queryset = queryset.filter(currency_code=currency_code)
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
- 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
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
- logger.error(f"Error getting transactions for user {user.id}: {e}")
421
- return []
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
- # Alias methods for backward compatibility with tests
425
- def credit_balance(self, request: 'BalanceUpdateRequest') -> 'ServiceOperationResult':
426
- """Alias for add_funds method."""
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
- 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
- )
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 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)
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)