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,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
- import logging
16
-
17
- logger = logging.getLogger(__name__)
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,83 +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
9
-
10
-
11
- class CurrencyManager(models.Manager):
12
- """Manager for Currency model with convenient query methods."""
13
-
14
- def active(self):
15
- """Get only active currencies."""
16
- return self.filter(is_active=True)
17
-
18
- def fiat(self):
19
- """Get only fiat currencies."""
20
- return self.filter(currency_type='fiat')
21
-
22
- def crypto(self):
23
- """Get only cryptocurrencies."""
24
- return self.filter(currency_type='crypto')
25
-
26
- def active_fiat(self):
27
- """Get active fiat currencies."""
28
- return self.filter(currency_type='fiat', is_active=True)
29
-
30
- def active_crypto(self):
31
- """Get active cryptocurrencies."""
32
- return self.filter(currency_type='crypto', is_active=True)
33
-
34
- def by_code(self, code: str):
35
- """Get currency by code (case insensitive)."""
36
- return self.filter(code__iexact=code).first()
37
-
38
- def supported_for_payments(self, min_amount: float = None):
39
- """Get currencies supported for payments."""
40
- queryset = self.active()
41
- if min_amount:
42
- queryset = queryset.filter(min_payment_amount__lte=min_amount)
43
- return queryset
44
-
45
- def recently_updated(self, hours: int = 24):
46
- """Get currencies updated within the last N hours."""
47
- threshold = timezone.now() - timedelta(hours=hours)
48
- return self.filter(rate_updated_at__gte=threshold)
49
-
50
- def outdated(self, days: int = 7):
51
- """Get currencies with outdated rates."""
52
- threshold = timezone.now() - timedelta(days=days)
53
- return self.filter(
54
- models.Q(rate_updated_at__lt=threshold) |
55
- models.Q(rate_updated_at__isnull=True)
56
- )
57
-
58
- def top_crypto_by_value(self, limit: int = 10):
59
- """Get top cryptocurrencies by USD value."""
60
- return self.active_crypto().order_by('-usd_rate')[:limit]
61
-
62
- def search(self, query: str):
63
- """Search currencies by code or name."""
64
- return self.filter(
65
- models.Q(code__icontains=query) |
66
- models.Q(name__icontains=query)
67
- )
68
-
69
-
70
- class CurrencyNetworkManager(models.Manager):
71
- """Manager for CurrencyNetwork model."""
72
-
73
- def active(self):
74
- """Get only active networks."""
75
- return self.filter(is_active=True)
76
-
77
- def for_currency(self, currency_code: str):
78
- """Get networks for a specific currency."""
79
- return self.filter(currency__code__iexact=currency_code)
80
-
81
- def active_for_currency(self, currency_code: str):
82
- """Get active networks for a specific currency."""
83
- return self.active().filter(currency__code__iexact=currency_code)
@@ -1,44 +0,0 @@
1
- """
2
- Payment manager for UniversalPayment model.
3
- """
4
-
5
- from django.db import models
6
-
7
-
8
- class UniversalPaymentManager(models.Manager):
9
- """Manager for UniversalPayment model."""
10
-
11
- def create_payment(self, user, amount_usd: float, currency_code: str, provider: str):
12
- """Create a new payment."""
13
- payment = self.create(
14
- user=user,
15
- amount_usd=amount_usd,
16
- currency_code=currency_code.upper(),
17
- provider=provider,
18
- status=self.model.PaymentStatus.PENDING
19
- )
20
- return payment
21
-
22
- def get_pending_payments(self, user=None):
23
- """Get pending payments for user or all users."""
24
- queryset = self.filter(status=self.model.PaymentStatus.PENDING)
25
- if user:
26
- queryset = queryset.filter(user=user)
27
- return queryset
28
-
29
- def get_completed_payments(self, user=None):
30
- """Get completed payments for user or all users."""
31
- queryset = self.filter(status=self.model.PaymentStatus.COMPLETED)
32
- if user:
33
- queryset = queryset.filter(user=user)
34
- return queryset
35
-
36
- def get_failed_payments(self, user=None):
37
- """Get failed/expired payments for user or all users."""
38
- queryset = self.filter(status__in=[
39
- self.model.PaymentStatus.FAILED,
40
- self.model.PaymentStatus.EXPIRED
41
- ])
42
- if user:
43
- queryset = queryset.filter(user=user)
44
- return queryset
@@ -1,37 +0,0 @@
1
- """
2
- Subscription managers.
3
- """
4
-
5
- from django.db import models
6
- from django.utils import timezone
7
-
8
-
9
- class SubscriptionManager(models.Manager):
10
- """Manager for Subscription model."""
11
-
12
- def get_active_subscriptions(self, user=None):
13
- """Get active subscriptions."""
14
- queryset = self.filter(
15
- status='active',
16
- expires_at__gt=timezone.now()
17
- )
18
- if user:
19
- queryset = queryset.filter(user=user)
20
- return queryset
21
-
22
- def get_expired_subscriptions(self, user=None):
23
- """Get expired subscriptions."""
24
- queryset = self.filter(
25
- expires_at__lte=timezone.now()
26
- )
27
- if user:
28
- queryset = queryset.filter(user=user)
29
- return queryset
30
-
31
-
32
- class EndpointGroupManager(models.Manager):
33
- """Manager for EndpointGroup model."""
34
-
35
- def get_active_groups(self):
36
- """Get active endpoint groups."""
37
- return self.filter(is_active=True)
@@ -1,29 +0,0 @@
1
- """
2
- Tariff managers.
3
- """
4
-
5
- from django.db import models
6
-
7
-
8
- class TariffManager(models.Manager):
9
- """Manager for Tariff model."""
10
-
11
- def get_active_tariffs(self):
12
- """Get active tariffs."""
13
- return self.filter(is_active=True).order_by('monthly_price')
14
-
15
- def get_free_tariffs(self):
16
- """Get free tariffs."""
17
- return self.filter(monthly_price=0, is_active=True)
18
-
19
- def get_paid_tariffs(self):
20
- """Get paid tariffs."""
21
- return self.filter(monthly_price__gt=0, is_active=True)
22
-
23
-
24
- class TariffEndpointGroupManager(models.Manager):
25
- """Manager for TariffEndpointGroup model."""
26
-
27
- def get_enabled_for_tariff(self, tariff):
28
- """Get enabled endpoint groups for tariff."""
29
- return self.filter(tariff=tariff, is_enabled=True)
@@ -1,73 +0,0 @@
1
- """
2
- Event sourcing models for the universal payments system.
3
- """
4
-
5
- from django.db import models
6
- from .base import UUIDTimestampedModel
7
-
8
-
9
- class PaymentEvent(UUIDTimestampedModel):
10
- """Event sourcing for payment operations - immutable audit trail."""
11
-
12
- class EventType(models.TextChoices):
13
- PAYMENT_CREATED = 'payment_created', 'Payment Created'
14
- WEBHOOK_RECEIVED = 'webhook_received', 'Webhook Received'
15
- WEBHOOK_PROCESSED = 'webhook_processed', 'Webhook Processed'
16
- BALANCE_UPDATED = 'balance_updated', 'Balance Updated'
17
- REFUND_PROCESSED = 'refund_processed', 'Refund Processed'
18
- STATUS_CHANGED = 'status_changed', 'Status Changed'
19
- ERROR_OCCURRED = 'error_occurred', 'Error Occurred'
20
-
21
- # Event identification
22
- payment_id = models.CharField(
23
- max_length=255,
24
- db_index=True,
25
- help_text="Payment identifier"
26
- )
27
- event_type = models.CharField(
28
- max_length=50,
29
- choices=EventType.choices,
30
- db_index=True,
31
- help_text="Type of event"
32
- )
33
- sequence_number = models.PositiveBigIntegerField(
34
- help_text="Sequential number per payment"
35
- )
36
-
37
- # Event data (JSON for flexibility)
38
- event_data = models.JSONField(
39
- help_text="Event data payload"
40
- )
41
-
42
- # Operational metadata
43
- processed_by = models.CharField(
44
- max_length=100,
45
- help_text="Worker/server that processed this event"
46
- )
47
- correlation_id = models.CharField(
48
- max_length=255,
49
- null=True,
50
- blank=True,
51
- help_text="Correlation ID for tracing"
52
- )
53
- idempotency_key = models.CharField(
54
- max_length=255,
55
- unique=True,
56
- help_text="Idempotency key to prevent duplicates"
57
- )
58
-
59
- class Meta:
60
- db_table = 'payment_events'
61
- verbose_name = "Payment Event"
62
- verbose_name_plural = "Payment Events"
63
- indexes = [
64
- models.Index(fields=['payment_id', 'sequence_number']),
65
- models.Index(fields=['event_type', 'created_at']),
66
- models.Index(fields=['idempotency_key']),
67
- models.Index(fields=['correlation_id']),
68
- models.Index(fields=['created_at']),
69
- ]
70
- ordering = ['sequence_number']
71
-
72
- def __str__(self):
73
- return f"Event {self.sequence_number}: {self.event_type} for {self.payment_id}"
@@ -1,56 +0,0 @@
1
- """
2
- DRF serializers for the universal payments system.
3
- """
4
-
5
- from .balance import (
6
- UserBalanceSerializer, TransactionSerializer, TransactionListSerializer
7
- )
8
- from .payments import (
9
- UniversalPaymentSerializer, PaymentCreateSerializer, PaymentListSerializer
10
- )
11
- from .subscriptions import (
12
- SubscriptionSerializer, SubscriptionCreateSerializer, SubscriptionListSerializer,
13
- EndpointGroupSerializer
14
- )
15
- from .api_keys import (
16
- APIKeySerializer, APIKeyCreateSerializer, APIKeyListSerializer
17
- )
18
- from .currencies import (
19
- CurrencySerializer, CurrencyNetworkSerializer, CurrencyListSerializer
20
- )
21
- from .tariffs import (
22
- TariffSerializer, TariffEndpointGroupSerializer, TariffListSerializer
23
- )
24
-
25
- __all__ = [
26
- # Balance
27
- 'UserBalanceSerializer',
28
- 'TransactionSerializer',
29
- 'TransactionListSerializer',
30
-
31
- # Payments
32
- 'UniversalPaymentSerializer',
33
- 'PaymentCreateSerializer',
34
- 'PaymentListSerializer',
35
-
36
- # Subscriptions
37
- 'SubscriptionSerializer',
38
- 'SubscriptionCreateSerializer',
39
- 'SubscriptionListSerializer',
40
- 'EndpointGroupSerializer',
41
-
42
- # API Keys
43
- 'APIKeySerializer',
44
- 'APIKeyCreateSerializer',
45
- 'APIKeyListSerializer',
46
-
47
- # Currencies
48
- 'CurrencySerializer',
49
- 'CurrencyNetworkSerializer',
50
- 'CurrencyListSerializer',
51
-
52
- # Tariffs
53
- 'TariffSerializer',
54
- 'TariffEndpointGroupSerializer',
55
- 'TariffListSerializer',
56
- ]