django-cfg 1.2.31__py3-none-any.whl → 1.3.3__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 (264) 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 -10
  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 +526 -222
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +465 -70
  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/cleanup_expired_data.py +419 -0
  39. django_cfg/apps/payments/management/commands/currency_stats.py +297 -225
  40. django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
  41. django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
  42. django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
  43. django_cfg/apps/payments/management/commands/test_providers.py +434 -0
  44. django_cfg/apps/payments/middleware/__init__.py +3 -1
  45. django_cfg/apps/payments/middleware/api_access.py +329 -222
  46. django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
  47. django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
  48. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  49. django_cfg/apps/payments/models/__init__.py +13 -18
  50. django_cfg/apps/payments/models/api_keys.py +121 -43
  51. django_cfg/apps/payments/models/balance.py +153 -115
  52. django_cfg/apps/payments/models/base.py +68 -15
  53. django_cfg/apps/payments/models/currencies.py +172 -148
  54. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  55. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  56. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  57. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  58. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  59. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  60. django_cfg/apps/payments/models/payments.py +235 -285
  61. django_cfg/apps/payments/models/subscriptions.py +257 -177
  62. django_cfg/apps/payments/models/tariffs.py +147 -40
  63. django_cfg/apps/payments/services/__init__.py +209 -56
  64. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  65. django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
  66. django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
  67. django_cfg/apps/payments/services/{cache/base.py → cache_service/interfaces.py} +3 -1
  68. django_cfg/apps/payments/services/cache_service/keys.py +49 -0
  69. django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
  70. django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
  71. django_cfg/apps/payments/services/core/__init__.py +10 -6
  72. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  73. django_cfg/apps/payments/services/core/base.py +166 -0
  74. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  75. django_cfg/apps/payments/services/core/payment_service.py +371 -465
  76. django_cfg/apps/payments/services/core/subscription_service.py +425 -481
  77. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  78. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  79. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  80. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  81. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  82. django_cfg/apps/payments/services/providers/base.py +234 -174
  83. django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
  84. django_cfg/apps/payments/services/providers/registry.py +367 -301
  85. django_cfg/apps/payments/services/types/__init__.py +78 -0
  86. django_cfg/apps/payments/services/types/data.py +177 -0
  87. django_cfg/apps/payments/services/types/requests.py +150 -0
  88. django_cfg/apps/payments/services/types/responses.py +156 -0
  89. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  90. django_cfg/apps/payments/signals/__init__.py +33 -8
  91. django_cfg/apps/payments/signals/api_key_signals.py +210 -129
  92. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  93. django_cfg/apps/payments/signals/payment_signals.py +128 -103
  94. django_cfg/apps/payments/signals/subscription_signals.py +194 -142
  95. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  96. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  97. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  98. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  99. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  100. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  101. django_cfg/apps/payments/urls.py +45 -48
  102. django_cfg/apps/payments/urls_admin.py +33 -42
  103. django_cfg/apps/payments/views/api/__init__.py +101 -0
  104. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  105. django_cfg/apps/payments/views/api/balances.py +381 -0
  106. django_cfg/apps/payments/views/api/base.py +298 -0
  107. django_cfg/apps/payments/views/api/currencies.py +402 -0
  108. django_cfg/apps/payments/views/api/payments.py +415 -0
  109. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  110. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  111. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  112. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  113. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  114. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  115. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  116. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  117. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  118. django_cfg/config.py +1 -1
  119. django_cfg/core/config.py +40 -4
  120. django_cfg/core/generation.py +25 -4
  121. django_cfg/core/integration/README.md +363 -0
  122. django_cfg/core/integration/__init__.py +47 -0
  123. django_cfg/core/integration/commands_collector.py +239 -0
  124. django_cfg/core/integration/display/__init__.py +15 -0
  125. django_cfg/core/integration/display/base.py +157 -0
  126. django_cfg/core/integration/display/ngrok.py +164 -0
  127. django_cfg/core/integration/display/startup.py +815 -0
  128. django_cfg/core/integration/url_integration.py +123 -0
  129. django_cfg/core/integration/version_checker.py +160 -0
  130. django_cfg/management/commands/auto_generate.py +4 -0
  131. django_cfg/management/commands/check_settings.py +6 -0
  132. django_cfg/management/commands/clear_constance.py +5 -2
  133. django_cfg/management/commands/create_token.py +6 -0
  134. django_cfg/management/commands/list_urls.py +6 -0
  135. django_cfg/management/commands/migrate_all.py +6 -0
  136. django_cfg/management/commands/migrator.py +3 -0
  137. django_cfg/management/commands/rundramatiq.py +6 -0
  138. django_cfg/management/commands/runserver_ngrok.py +51 -29
  139. django_cfg/management/commands/script.py +6 -0
  140. django_cfg/management/commands/show_config.py +12 -2
  141. django_cfg/management/commands/show_urls.py +4 -0
  142. django_cfg/management/commands/superuser.py +6 -0
  143. django_cfg/management/commands/task_clear.py +4 -1
  144. django_cfg/management/commands/task_status.py +3 -1
  145. django_cfg/management/commands/test_email.py +3 -0
  146. django_cfg/management/commands/test_telegram.py +6 -0
  147. django_cfg/management/commands/test_twilio.py +6 -0
  148. django_cfg/management/commands/tree.py +6 -0
  149. django_cfg/management/commands/validate_config.py +155 -149
  150. django_cfg/models/constance.py +31 -11
  151. django_cfg/models/payments.py +175 -492
  152. django_cfg/modules/django_logger.py +160 -146
  153. django_cfg/modules/django_unfold/dashboard.py +64 -16
  154. django_cfg/registry/core.py +1 -0
  155. django_cfg/template_archive/django_sample.zip +0 -0
  156. django_cfg/utils/smart_defaults.py +227 -570
  157. django_cfg/utils/toolkit.py +51 -11
  158. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/METADATA +4 -1
  159. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/RECORD +162 -185
  160. django_cfg/apps/payments/__init__.py +0 -8
  161. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  162. django_cfg/apps/payments/config/module.py +0 -70
  163. django_cfg/apps/payments/config/providers.py +0 -105
  164. django_cfg/apps/payments/config/settings.py +0 -96
  165. django_cfg/apps/payments/config/utils.py +0 -52
  166. django_cfg/apps/payments/decorators.py +0 -291
  167. django_cfg/apps/payments/management/commands/README.md +0 -146
  168. django_cfg/apps/payments/managers/__init__.py +0 -23
  169. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  170. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  171. django_cfg/apps/payments/managers/currency_manager.py +0 -306
  172. django_cfg/apps/payments/managers/payment_manager.py +0 -192
  173. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  174. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  175. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
  176. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
  177. django_cfg/apps/payments/models/events.py +0 -73
  178. django_cfg/apps/payments/serializers/__init__.py +0 -57
  179. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  180. django_cfg/apps/payments/serializers/balance.py +0 -59
  181. django_cfg/apps/payments/serializers/currencies.py +0 -63
  182. django_cfg/apps/payments/serializers/payments.py +0 -62
  183. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  184. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  185. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  186. django_cfg/apps/payments/services/cache/simple_cache.py +0 -135
  187. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  188. django_cfg/apps/payments/services/internal_types.py +0 -461
  189. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  190. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  191. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
  192. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  193. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
  194. django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
  195. django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
  196. django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
  197. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
  198. django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
  199. django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
  200. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
  201. django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
  202. django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
  203. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
  204. django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
  205. django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
  206. django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
  207. django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
  208. django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
  209. django_cfg/apps/payments/services/security/__init__.py +0 -34
  210. django_cfg/apps/payments/services/security/error_handler.py +0 -635
  211. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  212. django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
  213. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  214. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  215. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  216. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  217. django_cfg/apps/payments/tasks/__init__.py +0 -12
  218. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  219. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
  220. django_cfg/apps/payments/templates/payments/base.html +0 -182
  221. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  222. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  223. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
  224. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  225. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
  226. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
  227. django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
  228. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
  229. django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
  230. django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
  231. django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
  232. django_cfg/apps/payments/templates/payments/stats.html +0 -261
  233. django_cfg/apps/payments/templates/payments/test.html +0 -213
  234. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  235. django_cfg/apps/payments/utils/__init__.py +0 -43
  236. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  237. django_cfg/apps/payments/utils/config_utils.py +0 -239
  238. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  239. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  240. django_cfg/apps/payments/views/__init__.py +0 -63
  241. django_cfg/apps/payments/views/api_key_views.py +0 -164
  242. django_cfg/apps/payments/views/balance_views.py +0 -75
  243. django_cfg/apps/payments/views/currency_views.py +0 -122
  244. django_cfg/apps/payments/views/payment_views.py +0 -149
  245. django_cfg/apps/payments/views/subscription_views.py +0 -135
  246. django_cfg/apps/payments/views/tariff_views.py +0 -131
  247. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  248. django_cfg/apps/payments/views/templates/ajax.py +0 -451
  249. django_cfg/apps/payments/views/templates/base.py +0 -212
  250. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  251. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  252. django_cfg/apps/payments/views/templates/payment_management.py +0 -158
  253. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  254. django_cfg/apps/payments/views/templates/stats.py +0 -244
  255. django_cfg/apps/payments/views/templates/utils.py +0 -181
  256. django_cfg/apps/payments/views/webhook_views.py +0 -266
  257. django_cfg/apps/payments/viewsets.py +0 -66
  258. django_cfg/core/integration.py +0 -160
  259. django_cfg/template_archive/.gitignore +0 -1
  260. django_cfg/template_archive/__init__.py +0 -0
  261. django_cfg/urls.py +0 -33
  262. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/WHEEL +0 -0
  263. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/entry_points.txt +0 -0
  264. {django_cfg-1.2.31.dist-info → django_cfg-1.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,361 +0,0 @@
1
- """
2
- User balance manager with atomic operations.
3
-
4
- Following CRITICAL_REQUIREMENTS.md:
5
- - Atomic balance updates
6
- - Type safety
7
- - Event sourcing
8
- - Proper error handling
9
- """
10
-
11
- from django.db import models, transaction
12
- from django.utils import timezone
13
- from decimal import Decimal
14
- from typing import Optional, Dict, Any
15
- from django_cfg.modules.django_logger import get_logger
16
-
17
- logger = get_logger("balance_manager")
18
-
19
-
20
- class UserBalanceManager(models.Manager):
21
- """Manager for UserBalance with atomic operations."""
22
-
23
- def get_or_create_balance(self, user) -> 'UserBalance':
24
- """Get or create user balance atomically."""
25
- balance, created = self.get_or_create(
26
- user=user,
27
- defaults={
28
- 'amount_usd': Decimal('0'),
29
- 'reserved_usd': Decimal('0'),
30
- 'total_earned': Decimal('0'),
31
- 'total_spent': Decimal('0'),
32
- }
33
- )
34
-
35
- if created:
36
- logger.info(f"Created new balance for user {user.id}")
37
-
38
- return balance
39
-
40
- def add_funds(
41
- self,
42
- user,
43
- amount_usd: Decimal,
44
- description: str,
45
- reference_id: Optional[str] = None,
46
- payment=None
47
- ) -> Dict[str, Any]:
48
- """
49
- Add funds to user balance atomically.
50
-
51
- Returns:
52
- Dict with operation result and transaction details
53
- """
54
- if amount_usd <= 0:
55
- raise ValueError("Amount must be positive")
56
-
57
- with transaction.atomic():
58
- # Get or create balance with row lock
59
- balance = self.select_for_update().get_or_create_balance(user)
60
-
61
- # Store old values for transaction record
62
- old_balance = balance.amount_usd
63
- old_earned = balance.total_earned
64
-
65
- # Update balance
66
- balance.amount_usd += amount_usd
67
- balance.total_earned += amount_usd
68
- balance.last_transaction_at = timezone.now()
69
- balance.save()
70
-
71
- # Create transaction record
72
- from ..models.balance import Transaction
73
- transaction_record = Transaction.objects.create(
74
- user=user,
75
- amount_usd=amount_usd,
76
- transaction_type=Transaction.TypeChoices.CREDIT,
77
- description=description,
78
- payment=payment,
79
- reference_id=reference_id,
80
- balance_before=old_balance,
81
- balance_after=balance.amount_usd,
82
- metadata={
83
- 'total_earned_before': str(old_earned),
84
- 'total_earned_after': str(balance.total_earned),
85
- }
86
- )
87
-
88
- logger.info(
89
- f"Added ${amount_usd} to user {user.id} balance. "
90
- f"New balance: ${balance.amount_usd}"
91
- )
92
-
93
- return {
94
- 'success': True,
95
- 'old_balance': old_balance,
96
- 'new_balance': balance.amount_usd,
97
- 'amount_added': amount_usd,
98
- 'transaction_id': str(transaction_record.id),
99
- 'balance_obj': balance
100
- }
101
-
102
- def debit_funds(
103
- self,
104
- user,
105
- amount_usd: Decimal,
106
- description: str,
107
- reference_id: Optional[str] = None,
108
- allow_overdraft: bool = False
109
- ) -> Dict[str, Any]:
110
- """
111
- Debit funds from user balance atomically.
112
-
113
- Args:
114
- user: User object
115
- amount_usd: Amount to debit (positive value)
116
- description: Transaction description
117
- reference_id: Optional reference ID
118
- allow_overdraft: Allow negative balance
119
-
120
- Returns:
121
- Dict with operation result
122
- """
123
- if amount_usd <= 0:
124
- raise ValueError("Amount must be positive")
125
-
126
- with transaction.atomic():
127
- # Get balance with row lock
128
- balance = self.select_for_update().get_or_create_balance(user)
129
-
130
- # Check sufficient funds
131
- if not allow_overdraft and balance.amount_usd < amount_usd:
132
- from ..models.exceptions import InsufficientFundsError
133
- from ..models.pydantic_models import MoneyAmount
134
- from ..models import CurrencyChoices
135
-
136
- raise InsufficientFundsError(
137
- message=f"Insufficient funds: ${balance.amount_usd} < ${amount_usd}",
138
- required_amount=MoneyAmount(amount=amount_usd, currency=CurrencyChoices.USD),
139
- available_amount=MoneyAmount(amount=balance.amount_usd, currency=CurrencyChoices.USD),
140
- user_id=user.id
141
- )
142
-
143
- # Store old values
144
- old_balance = balance.amount_usd
145
- old_spent = balance.total_spent
146
-
147
- # Update balance
148
- balance.amount_usd -= amount_usd
149
- balance.total_spent += amount_usd
150
- balance.last_transaction_at = timezone.now()
151
- balance.save()
152
-
153
- # Create transaction record
154
- from ..models.balance import Transaction
155
- transaction_record = Transaction.objects.create(
156
- user=user,
157
- amount_usd=-amount_usd, # Negative for debit
158
- transaction_type=Transaction.TypeChoices.DEBIT,
159
- description=description,
160
- reference_id=reference_id,
161
- balance_before=old_balance,
162
- balance_after=balance.amount_usd,
163
- metadata={
164
- 'total_spent_before': str(old_spent),
165
- 'total_spent_after': str(balance.total_spent),
166
- 'allow_overdraft': allow_overdraft,
167
- }
168
- )
169
-
170
- logger.info(
171
- f"Debited ${amount_usd} from user {user.id} balance. "
172
- f"New balance: ${balance.amount_usd}"
173
- )
174
-
175
- return {
176
- 'success': True,
177
- 'old_balance': old_balance,
178
- 'new_balance': balance.amount_usd,
179
- 'amount_debited': amount_usd,
180
- 'transaction_id': str(transaction_record.id),
181
- 'balance_obj': balance
182
- }
183
-
184
- def hold_funds(
185
- self,
186
- user,
187
- amount_usd: Decimal,
188
- description: str,
189
- reference_id: Optional[str] = None
190
- ) -> Dict[str, Any]:
191
- """
192
- Hold funds (move from available to reserved).
193
-
194
- Args:
195
- user: User object
196
- amount_usd: Amount to hold
197
- description: Hold description
198
- reference_id: Optional reference ID
199
-
200
- Returns:
201
- Dict with operation result
202
- """
203
- if amount_usd <= 0:
204
- raise ValueError("Amount must be positive")
205
-
206
- with transaction.atomic():
207
- # Get balance with row lock
208
- balance = self.select_for_update().get_or_create_balance(user)
209
-
210
- # Check sufficient available funds
211
- if balance.amount_usd < amount_usd:
212
- from ..models.exceptions import InsufficientFundsError
213
- from ..models.pydantic_models import MoneyAmount
214
- from ..models import CurrencyChoices
215
-
216
- raise InsufficientFundsError(
217
- message=f"Insufficient available funds for hold: ${balance.amount_usd} < ${amount_usd}",
218
- required_amount=MoneyAmount(amount=amount_usd, currency=CurrencyChoices.USD),
219
- available_amount=MoneyAmount(amount=balance.amount_usd, currency=CurrencyChoices.USD),
220
- user_id=user.id
221
- )
222
-
223
- # Store old values
224
- old_available = balance.amount_usd
225
- old_reserved = balance.reserved_usd
226
-
227
- # Move funds from available to reserved
228
- balance.amount_usd -= amount_usd
229
- balance.reserved_usd += amount_usd
230
- balance.last_transaction_at = timezone.now()
231
- balance.save()
232
-
233
- # Create transaction record
234
- from ..models.balance import Transaction
235
- transaction_record = Transaction.objects.create(
236
- user=user,
237
- amount_usd=amount_usd,
238
- transaction_type=Transaction.TypeChoices.HOLD,
239
- description=description,
240
- reference_id=reference_id,
241
- balance_before=old_available,
242
- balance_after=balance.amount_usd,
243
- metadata={
244
- 'reserved_before': str(old_reserved),
245
- 'reserved_after': str(balance.reserved_usd),
246
- 'operation': 'hold_funds',
247
- }
248
- )
249
-
250
- logger.info(
251
- f"Held ${amount_usd} for user {user.id}. "
252
- f"Available: ${balance.amount_usd}, Reserved: ${balance.reserved_usd}"
253
- )
254
-
255
- return {
256
- 'success': True,
257
- 'amount_held': amount_usd,
258
- 'available_balance': balance.amount_usd,
259
- 'reserved_balance': balance.reserved_usd,
260
- 'transaction_id': str(transaction_record.id),
261
- 'balance_obj': balance
262
- }
263
-
264
- def release_funds(
265
- self,
266
- user,
267
- amount_usd: Decimal,
268
- description: str,
269
- reference_id: Optional[str] = None,
270
- refund_to_available: bool = True
271
- ) -> Dict[str, Any]:
272
- """
273
- Release held funds.
274
-
275
- Args:
276
- user: User object
277
- amount_usd: Amount to release
278
- description: Release description
279
- reference_id: Optional reference ID
280
- refund_to_available: If True, move to available; if False, remove entirely
281
-
282
- Returns:
283
- Dict with operation result
284
- """
285
- if amount_usd <= 0:
286
- raise ValueError("Amount must be positive")
287
-
288
- with transaction.atomic():
289
- # Get balance with row lock
290
- balance = self.select_for_update().get_or_create_balance(user)
291
-
292
- # Check sufficient reserved funds
293
- if balance.reserved_usd < amount_usd:
294
- raise ValueError(
295
- f"Insufficient reserved funds: ${balance.reserved_usd} < ${amount_usd}"
296
- )
297
-
298
- # Store old values
299
- old_available = balance.amount_usd
300
- old_reserved = balance.reserved_usd
301
-
302
- # Release funds
303
- balance.reserved_usd -= amount_usd
304
- if refund_to_available:
305
- balance.amount_usd += amount_usd
306
- else:
307
- # Funds are consumed (e.g., for payment)
308
- balance.total_spent += amount_usd
309
-
310
- balance.last_transaction_at = timezone.now()
311
- balance.save()
312
-
313
- # Create transaction record
314
- from ..models.balance import Transaction
315
- transaction_record = Transaction.objects.create(
316
- user=user,
317
- amount_usd=amount_usd if refund_to_available else -amount_usd,
318
- transaction_type=Transaction.TypeChoices.RELEASE,
319
- description=description,
320
- reference_id=reference_id,
321
- balance_before=old_available,
322
- balance_after=balance.amount_usd,
323
- metadata={
324
- 'reserved_before': str(old_reserved),
325
- 'reserved_after': str(balance.reserved_usd),
326
- 'refund_to_available': refund_to_available,
327
- 'operation': 'release_funds',
328
- }
329
- )
330
-
331
- action = "refunded to available" if refund_to_available else "consumed"
332
- logger.info(
333
- f"Released ${amount_usd} for user {user.id} ({action}). "
334
- f"Available: ${balance.amount_usd}, Reserved: ${balance.reserved_usd}"
335
- )
336
-
337
- return {
338
- 'success': True,
339
- 'amount_released': amount_usd,
340
- 'refund_to_available': refund_to_available,
341
- 'available_balance': balance.amount_usd,
342
- 'reserved_balance': balance.reserved_usd,
343
- 'transaction_id': str(transaction_record.id),
344
- 'balance_obj': balance
345
- }
346
-
347
- def get_balance_summary(self, user) -> Dict[str, Any]:
348
- """Get comprehensive balance summary for user."""
349
- balance = self.get_or_create_balance(user)
350
-
351
- return {
352
- 'user_id': user.id,
353
- 'available_balance': balance.amount_usd,
354
- 'reserved_balance': balance.reserved_usd,
355
- 'total_balance': balance.total_balance,
356
- 'total_earned': balance.total_earned,
357
- 'total_spent': balance.total_spent,
358
- 'last_transaction_at': balance.last_transaction_at,
359
- 'created_at': balance.created_at,
360
- 'updated_at': balance.updated_at,
361
- }
@@ -1,306 +0,0 @@
1
- """
2
- Manager for Currency model.
3
- """
4
-
5
- from django.db import models
6
- from django.utils import timezone
7
- from datetime import timedelta
8
- from typing import List, Optional, TYPE_CHECKING
9
- from decimal import Decimal
10
-
11
- from django_cfg.modules.django_logger import get_logger
12
- from django_cfg.modules.django_currency import convert_currency, get_exchange_rate, CurrencyError
13
-
14
- if TYPE_CHECKING:
15
- from ..services.internal_types import CurrencyOptionModel
16
-
17
- logger = get_logger("currency_manager")
18
-
19
-
20
- class CurrencyManager(models.Manager):
21
- """Manager for clean Currency model."""
22
-
23
- def fiat(self):
24
- """Get only fiat currencies."""
25
- return self.filter(currency_type='fiat')
26
-
27
- def crypto(self):
28
- """Get only cryptocurrencies."""
29
- return self.filter(currency_type='crypto')
30
-
31
- def by_code(self, code: str):
32
- """Get currency by code (case insensitive)."""
33
- return self.filter(code__iexact=code).first()
34
-
35
- def search(self, query: str):
36
- """Search currencies by code or name."""
37
- return self.filter(
38
- models.Q(code__icontains=query) |
39
- models.Q(name__icontains=query)
40
- )
41
-
42
- def get_usd_rate(self, currency_code_or_instance, force_refresh: bool = False) -> float:
43
- """
44
- Get USD exchange rate for currency (with 24h cache).
45
-
46
- Args:
47
- currency_code_or_instance: Currency code (e.g., 'BTC') or Currency instance
48
- force_refresh: If True, skip cache and fetch fresh rate
49
-
50
- Returns:
51
- float: 1 CURRENCY = X USD
52
- """
53
- try:
54
- # Handle both Currency instance and string code
55
- if hasattr(currency_code_or_instance, 'code'):
56
- # Currency instance passed
57
- currency = currency_code_or_instance
58
- currency_code = currency.code
59
- else:
60
- # String code passed
61
- currency_code = str(currency_code_or_instance).upper()
62
- currency = self.filter(code=currency_code).first()
63
-
64
- # Return cached rate if fresh and not forcing refresh
65
- if not force_refresh and currency and currency.usd_rate is not None and currency.rate_updated_at:
66
- # Check if cache is still fresh (24 hours)
67
- if timezone.now() - currency.rate_updated_at < timedelta(hours=24):
68
- logger.debug(f"Using cached USD rate for {currency_code}: ${float(currency.usd_rate):.8f}")
69
- return float(currency.usd_rate)
70
-
71
- # Cache miss, expired, or forced refresh - fetch fresh rate
72
- logger.info(f"Fetching fresh USD rate for {currency_code} (force_refresh={force_refresh})")
73
- rate = get_exchange_rate(currency_code, 'USD')
74
- rate_decimal = Decimal(str(rate)).quantize(Decimal('0.00000001'))
75
-
76
- # Update cache
77
- if currency:
78
- currency.usd_rate = rate_decimal
79
- currency.rate_updated_at = timezone.now()
80
- currency.save(update_fields=['usd_rate', 'rate_updated_at'])
81
- logger.info(f"Updated USD rate for {currency_code}: ${rate:.8f}")
82
- else:
83
- logger.warning(f"Currency {currency_code} not found in database for rate caching")
84
-
85
- return round(rate, 8)
86
-
87
- except CurrencyError as e:
88
- logger.warning(f"Failed to get USD rate for {currency_code}: {e}")
89
- # Return cached rate if available, even if stale
90
- if currency and currency.usd_rate is not None:
91
- logger.info(f"Using stale cached rate for {currency_code} due to API error")
92
- return float(currency.usd_rate)
93
- return 0.0
94
-
95
- def get_tokens_per_usd(self, currency_code: str) -> float:
96
- """Get how many tokens you can buy for 1 USD."""
97
- usd_rate = self.get_usd_rate(currency_code)
98
- if usd_rate > 0:
99
- return round(1.0 / usd_rate, 8)
100
- return 0.0
101
-
102
- def convert_to_usd(self, amount: float, currency_code: str) -> float:
103
- """Convert currency amount to USD."""
104
- usd_rate = self.get_usd_rate(currency_code)
105
- return round(amount * usd_rate, 2)
106
-
107
- def convert_from_usd(self, usd_amount: float, currency_code: str) -> float:
108
- """Convert USD amount to target currency."""
109
- tokens_per_usd = self.get_tokens_per_usd(currency_code)
110
- return round(usd_amount * tokens_per_usd, 8)
111
-
112
- def get_or_create_normalized(self, code: str, defaults: dict = None):
113
- """Simple get_or_create with uppercase code normalization."""
114
- normalized_code = code.upper().strip() if code else ''
115
- if not normalized_code:
116
- raise ValueError(f"Empty currency code: '{code}'")
117
-
118
- creation_defaults = defaults or {}
119
- creation_defaults['code'] = normalized_code
120
-
121
- return self.get_or_create(code__iexact=normalized_code, defaults=creation_defaults)
122
-
123
-
124
- class NetworkManager(models.Manager):
125
- """Manager for Network model."""
126
-
127
- def by_code(self, code: str):
128
- """Get network by code (case insensitive)."""
129
- return self.filter(code__iexact=code).first()
130
-
131
- def get_or_create_normalized(self, code: str, defaults: dict = None):
132
- """Get or create network with normalized code."""
133
- normalized_code = code.lower().strip() if code else ''
134
- if not normalized_code:
135
- raise ValueError(f"Empty network code: '{code}'")
136
-
137
- creation_defaults = defaults or {}
138
- creation_defaults['code'] = normalized_code
139
-
140
- return self.get_or_create(code__iexact=normalized_code, defaults=creation_defaults)
141
-
142
-
143
- class ProviderCurrencyManager(models.Manager):
144
- """Manager for ProviderCurrency model."""
145
-
146
- def enabled(self):
147
- """Get only enabled provider currencies."""
148
- return self.filter(is_enabled=True)
149
-
150
- def for_provider(self, provider_name: str):
151
- """Get currencies for specific provider."""
152
- return self.filter(provider_name__iexact=provider_name)
153
-
154
- def for_base_currency(self, currency_code: str):
155
- """Get provider currencies for base currency."""
156
- return self.filter(base_currency__code__iexact=currency_code)
157
-
158
- def for_network(self, network_code: str):
159
- """Get provider currencies for network."""
160
- return self.filter(network__code__iexact=network_code)
161
-
162
- def enabled_for_provider(self, provider_name: str):
163
- """Get enabled currencies for provider."""
164
- return self.enabled().filter(provider_name__iexact=provider_name)
165
-
166
- def popular(self):
167
- """Get popular currencies."""
168
- return self.filter(is_popular=True)
169
-
170
- def stable(self):
171
- """Get stable currencies."""
172
- return self.filter(is_stable=True)
173
-
174
- def get_currency_options_for_provider(self, provider_name: str):
175
- """
176
- Get flat list of currency options for single select dropdown.
177
-
178
- Returns:
179
- List[dict]: List of currency option dictionaries
180
- """
181
- provider_currencies = self.enabled_for_provider(provider_name).select_related(
182
- 'base_currency', 'network'
183
- ).order_by('is_popular', 'is_stable', 'base_currency__code', 'network__code')
184
-
185
- options = []
186
- for pc in provider_currencies:
187
- # Create display name: "USDT (Ethereum)" or "BTC" for native currencies
188
- if pc.network:
189
- display_name = f"{pc.base_currency.code} ({pc.network.name})"
190
- else:
191
- display_name = pc.base_currency.code
192
-
193
- # Get exchange rates using Currency manager
194
- from ..models import Currency
195
- usd_rate = Currency.objects.get_usd_rate(pc.base_currency.code)
196
- tokens_per_usd = Currency.objects.get_tokens_per_usd(pc.base_currency.code)
197
-
198
- option = {
199
- 'provider_currency_code': pc.provider_currency_code,
200
- 'display_name': display_name,
201
- 'base_currency_code': pc.base_currency.code,
202
- 'base_currency_name': pc.base_currency.name,
203
- 'network_code': pc.network.code if pc.network else None,
204
- 'network_name': pc.network.name if pc.network else None,
205
- 'currency_type': pc.base_currency.currency_type,
206
- 'is_popular': pc.is_popular,
207
- 'is_stable': pc.is_stable,
208
- 'available_for_payment': pc.available_for_payment,
209
- 'available_for_payout': pc.available_for_payout,
210
- 'min_amount': str(pc.min_amount) if pc.min_amount else None,
211
- 'max_amount': str(pc.max_amount) if pc.max_amount else None,
212
- 'logo_url': pc.logo_url,
213
- # Exchange rates
214
- 'usd_rate': usd_rate,
215
- 'tokens_per_usd': tokens_per_usd
216
- }
217
- options.append(option)
218
-
219
- # Sort: popular first, then stable, then alphabetically
220
- def sort_key(option):
221
- return (
222
- 0 if option['is_popular'] else 1, # Popular first
223
- 0 if option['is_stable'] else 1, # Then stable
224
- option['base_currency_code'], # Then by base currency
225
- option['network_name'] or '' # Then by network
226
- )
227
-
228
- options.sort(key=sort_key)
229
- return options
230
-
231
- def get_usd_rates_for_provider(self, provider_name: str):
232
- """
233
- Get USD exchange rates for all provider currencies.
234
-
235
- Returns:
236
- dict: {provider_currency_code: {'rate': 0.0001, 'tokens_per_usd': 10000}}
237
- """
238
- provider_currencies = self.enabled_for_provider(provider_name).select_related('base_currency')
239
- rates = {}
240
-
241
- for pc in provider_currencies:
242
- try:
243
- # Get rate: 1 BASE_CURRENCY = X USD
244
- usd_rate = get_exchange_rate(pc.base_currency.code, 'USD')
245
-
246
- # Calculate tokens per 1 USD
247
- if usd_rate > 0:
248
- tokens_per_usd = 1.0 / usd_rate
249
- else:
250
- tokens_per_usd = 0.0
251
-
252
- rates[pc.provider_currency_code] = {
253
- 'usd_rate': round(usd_rate, 8),
254
- 'tokens_per_usd': round(tokens_per_usd, 2),
255
- 'base_currency': pc.base_currency.code,
256
- 'updated_at': timezone.now().isoformat()
257
- }
258
-
259
- except CurrencyError as e:
260
- logger.warning(f"Failed to get rate for {pc.base_currency.code}: {e}")
261
- rates[pc.provider_currency_code] = {
262
- 'usd_rate': 0.0,
263
- 'tokens_per_usd': 0.0,
264
- 'base_currency': pc.base_currency.code,
265
- 'error': str(e)
266
- }
267
-
268
- return rates
269
-
270
- def convert_amount(self, amount: float, from_currency_code: str, to_currency: str = 'USD'):
271
- """
272
- Convert amount from provider currency to target currency.
273
-
274
- Args:
275
- amount: Amount to convert
276
- from_currency_code: Provider currency code (e.g., 'USDTERC20')
277
- to_currency: Target currency (default: 'USD')
278
-
279
- Returns:
280
- dict: {'amount': converted_amount, 'rate': exchange_rate, 'from': base_currency}
281
- """
282
- try:
283
- # Find provider currency and get base currency
284
- pc = self.get(provider_currency_code=from_currency_code)
285
- base_currency = pc.base_currency.code
286
-
287
- # Convert via base currency
288
- converted_amount = convert_currency(amount, base_currency, to_currency)
289
- rate = get_exchange_rate(base_currency, to_currency)
290
-
291
- return {
292
- 'amount': round(converted_amount, 2),
293
- 'rate': round(rate, 8),
294
- 'from': base_currency,
295
- 'to': to_currency,
296
- 'original_amount': amount,
297
- 'provider_code': from_currency_code
298
- }
299
-
300
- except (CurrencyError, self.model.DoesNotExist) as e:
301
- logger.error(f"Conversion failed for {from_currency_code}: {e}")
302
- return {
303
- 'amount': 0.0,
304
- 'rate': 0.0,
305
- 'error': str(e)
306
- }