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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -9
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +600 -108
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +470 -64
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
- django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +16 -20
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +207 -67
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -284
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +344 -468
- django_cfg/apps/payments/services/core/subscription_service.py +425 -484
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +232 -71
- django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
- django_cfg/apps/payments/services/providers/registry.py +429 -80
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +211 -130
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +129 -98
- django_cfg/apps/payments/signals/subscription_signals.py +195 -143
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +46 -47
- django_cfg/apps/payments/urls_admin.py +49 -0
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +75 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -498
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +65 -12
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -178
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/managers/__init__.py +0 -22
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -83
- django_cfg/apps/payments/managers/payment_manager.py +0 -44
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -56
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -55
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -297
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -637
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/urls_templates.py +0 -52
- django_cfg/apps/payments/utils/__init__.py +0 -45
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -245
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -62
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -111
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -312
- django_cfg/apps/payments/views/templates/base.py +0 -204
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -164
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -240
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -65
- django_cfg/core/integration.py +0 -160
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,599 @@
|
|
1
|
+
"""
|
2
|
+
Balance and transaction managers for the Universal Payment System v2.0.
|
3
|
+
|
4
|
+
Optimized querysets and managers for balance and transaction operations.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from django.db import models
|
8
|
+
from django.utils import timezone
|
9
|
+
from django_cfg.modules.django_logger import get_logger
|
10
|
+
|
11
|
+
logger = get_logger("balance_managers")
|
12
|
+
|
13
|
+
|
14
|
+
class UserBalanceManager(models.Manager):
|
15
|
+
"""
|
16
|
+
Manager for UserBalance operations.
|
17
|
+
|
18
|
+
Provides methods for balance management and atomic operations.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def get_or_create_for_user(self, user):
|
22
|
+
"""
|
23
|
+
Get or create balance for user.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
user: User instance
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
UserBalance: Balance instance
|
30
|
+
"""
|
31
|
+
balance, created = self.get_or_create(
|
32
|
+
user=user,
|
33
|
+
defaults={'balance_usd': 0.0}
|
34
|
+
)
|
35
|
+
|
36
|
+
if created:
|
37
|
+
logger.info(f"Created new balance for user", extra={
|
38
|
+
'user_id': user.id,
|
39
|
+
'initial_balance': 0.0
|
40
|
+
})
|
41
|
+
|
42
|
+
return balance
|
43
|
+
|
44
|
+
def add_funds_to_user(self, user, amount, transaction_type='deposit',
|
45
|
+
description=None, payment_id=None):
|
46
|
+
"""
|
47
|
+
Add funds to user balance atomically (business logic in manager).
|
48
|
+
|
49
|
+
Args:
|
50
|
+
user: User instance
|
51
|
+
amount: Amount to add (positive)
|
52
|
+
transaction_type: Type of transaction
|
53
|
+
description: Transaction description
|
54
|
+
payment_id: Related payment ID
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
Transaction: Created transaction record
|
58
|
+
"""
|
59
|
+
if amount <= 0:
|
60
|
+
raise ValueError("Amount must be positive")
|
61
|
+
|
62
|
+
# Get or create balance
|
63
|
+
balance = self.get_or_create_for_user(user)
|
64
|
+
|
65
|
+
with models.transaction.atomic():
|
66
|
+
# Update balance
|
67
|
+
balance.balance_usd += amount
|
68
|
+
balance.total_deposited += amount
|
69
|
+
balance.last_transaction_at = timezone.now()
|
70
|
+
balance.save(update_fields=[
|
71
|
+
'balance_usd', 'total_deposited', 'last_transaction_at', 'updated_at'
|
72
|
+
])
|
73
|
+
|
74
|
+
# Create transaction record
|
75
|
+
from ..balance import Transaction
|
76
|
+
transaction_record = Transaction.objects.create(
|
77
|
+
user=user,
|
78
|
+
transaction_type=transaction_type,
|
79
|
+
amount_usd=amount,
|
80
|
+
balance_after=balance.balance_usd,
|
81
|
+
description=description or f"Added ${amount:.2f} to balance",
|
82
|
+
payment_id=payment_id
|
83
|
+
)
|
84
|
+
|
85
|
+
logger.info(f"Added funds to user balance", extra={
|
86
|
+
'user_id': user.id,
|
87
|
+
'amount': amount,
|
88
|
+
'new_balance': balance.balance_usd,
|
89
|
+
'transaction_id': str(transaction_record.id),
|
90
|
+
'payment_id': payment_id
|
91
|
+
})
|
92
|
+
|
93
|
+
# Update analytics
|
94
|
+
self.update_balance_analytics(user, amount)
|
95
|
+
|
96
|
+
return transaction_record
|
97
|
+
|
98
|
+
def subtract_funds_from_user(self, user, amount, transaction_type='withdrawal',
|
99
|
+
description=None, payment_id=None):
|
100
|
+
"""
|
101
|
+
Subtract funds from user balance atomically (business logic in manager).
|
102
|
+
|
103
|
+
Args:
|
104
|
+
user: User instance
|
105
|
+
amount: Amount to subtract (positive)
|
106
|
+
transaction_type: Type of transaction
|
107
|
+
description: Transaction description
|
108
|
+
payment_id: Related payment ID
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
Transaction: Created transaction record
|
112
|
+
"""
|
113
|
+
if amount <= 0:
|
114
|
+
raise ValueError("Amount must be positive")
|
115
|
+
|
116
|
+
# Get balance
|
117
|
+
try:
|
118
|
+
balance = self.get(user=user)
|
119
|
+
except self.model.DoesNotExist:
|
120
|
+
raise ValueError("User has no balance record")
|
121
|
+
|
122
|
+
if amount > balance.balance_usd:
|
123
|
+
raise ValueError(f"Insufficient balance: ${balance.balance_usd:.2f} < ${amount:.2f}")
|
124
|
+
|
125
|
+
with models.transaction.atomic():
|
126
|
+
# Update balance
|
127
|
+
balance.balance_usd -= amount
|
128
|
+
balance.total_spent += amount
|
129
|
+
balance.last_transaction_at = timezone.now()
|
130
|
+
balance.save(update_fields=[
|
131
|
+
'balance_usd', 'total_spent', 'last_transaction_at', 'updated_at'
|
132
|
+
])
|
133
|
+
|
134
|
+
# Create transaction record
|
135
|
+
from ..balance import Transaction
|
136
|
+
transaction_record = Transaction.objects.create(
|
137
|
+
user=user,
|
138
|
+
transaction_type=transaction_type,
|
139
|
+
amount_usd=-amount, # Negative for withdrawals
|
140
|
+
balance_after=balance.balance_usd,
|
141
|
+
description=description or f"Subtracted ${amount:.2f} from balance",
|
142
|
+
payment_id=payment_id
|
143
|
+
)
|
144
|
+
|
145
|
+
logger.info(f"Subtracted funds from user balance", extra={
|
146
|
+
'user_id': user.id,
|
147
|
+
'amount': amount,
|
148
|
+
'new_balance': balance.balance_usd,
|
149
|
+
'transaction_id': str(transaction_record.id),
|
150
|
+
'payment_id': payment_id
|
151
|
+
})
|
152
|
+
|
153
|
+
# Update analytics
|
154
|
+
self.update_balance_analytics(user, -amount)
|
155
|
+
|
156
|
+
return transaction_record
|
157
|
+
|
158
|
+
def update_balance_analytics(self, user, balance_change):
|
159
|
+
"""
|
160
|
+
Update balance analytics in cache (moved from signals).
|
161
|
+
|
162
|
+
Args:
|
163
|
+
user: User instance
|
164
|
+
balance_change: Amount of balance change
|
165
|
+
"""
|
166
|
+
try:
|
167
|
+
from django.core.cache import cache
|
168
|
+
|
169
|
+
user_id = user.id
|
170
|
+
|
171
|
+
# Update balance history
|
172
|
+
history_key = f"balance_history:{user_id}"
|
173
|
+
history = cache.get(history_key, [])
|
174
|
+
|
175
|
+
history.append({
|
176
|
+
'timestamp': timezone.now().isoformat(),
|
177
|
+
'balance': self.get_or_create_for_user(user).balance_usd,
|
178
|
+
'change': balance_change
|
179
|
+
})
|
180
|
+
|
181
|
+
# Keep only last 100 entries
|
182
|
+
if len(history) > 100:
|
183
|
+
history = history[-100:]
|
184
|
+
|
185
|
+
cache.set(history_key, history, timeout=86400 * 7) # 7 days
|
186
|
+
|
187
|
+
# Update daily totals
|
188
|
+
today = timezone.now().date().isoformat()
|
189
|
+
daily_key = f"balance_changes:{user_id}:{today}"
|
190
|
+
|
191
|
+
daily_data = cache.get(daily_key, {'total_change': 0.0, 'transaction_count': 0})
|
192
|
+
daily_data['total_change'] += balance_change
|
193
|
+
daily_data['transaction_count'] += 1
|
194
|
+
|
195
|
+
cache.set(daily_key, daily_data, timeout=86400 * 2) # 2 days
|
196
|
+
|
197
|
+
logger.debug(f"Updated balance analytics", extra={
|
198
|
+
'user_id': user_id,
|
199
|
+
'balance_change': balance_change,
|
200
|
+
'total_change_today': daily_data['total_change']
|
201
|
+
})
|
202
|
+
|
203
|
+
except Exception as e:
|
204
|
+
logger.warning(f"Failed to update balance analytics", extra={
|
205
|
+
'user_id': user.id,
|
206
|
+
'error': str(e)
|
207
|
+
})
|
208
|
+
|
209
|
+
|
210
|
+
class TransactionQuerySet(models.QuerySet):
|
211
|
+
"""
|
212
|
+
Optimized queryset for transaction operations.
|
213
|
+
|
214
|
+
Provides efficient queries for transaction history and analysis.
|
215
|
+
"""
|
216
|
+
|
217
|
+
def optimized(self):
|
218
|
+
"""Prevent N+1 queries with select_related."""
|
219
|
+
return self.select_related('user')
|
220
|
+
|
221
|
+
def by_user(self, user):
|
222
|
+
"""Filter transactions by user."""
|
223
|
+
return self.filter(user=user)
|
224
|
+
|
225
|
+
def by_type(self, transaction_type):
|
226
|
+
"""Filter by transaction type."""
|
227
|
+
return self.filter(transaction_type=transaction_type)
|
228
|
+
|
229
|
+
def by_payment(self, payment_id):
|
230
|
+
"""Filter by related payment ID."""
|
231
|
+
return self.filter(payment_id=payment_id)
|
232
|
+
|
233
|
+
# Transaction type filters
|
234
|
+
def deposits(self):
|
235
|
+
"""Get deposit transactions (positive amounts)."""
|
236
|
+
return self.filter(transaction_type='deposit', amount_usd__gt=0)
|
237
|
+
|
238
|
+
def withdrawals(self):
|
239
|
+
"""Get withdrawal transactions (negative amounts)."""
|
240
|
+
return self.filter(transaction_type='withdrawal', amount_usd__lt=0)
|
241
|
+
|
242
|
+
def payments(self):
|
243
|
+
"""Get payment-related transactions."""
|
244
|
+
return self.filter(transaction_type='payment')
|
245
|
+
|
246
|
+
def refunds(self):
|
247
|
+
"""Get refund transactions."""
|
248
|
+
return self.filter(transaction_type='refund')
|
249
|
+
|
250
|
+
def fees(self):
|
251
|
+
"""Get fee transactions."""
|
252
|
+
return self.filter(transaction_type='fee')
|
253
|
+
|
254
|
+
def bonuses(self):
|
255
|
+
"""Get bonus transactions."""
|
256
|
+
return self.filter(transaction_type='bonus')
|
257
|
+
|
258
|
+
def adjustments(self):
|
259
|
+
"""Get adjustment transactions."""
|
260
|
+
return self.filter(transaction_type='adjustment')
|
261
|
+
|
262
|
+
# Amount-based filters
|
263
|
+
def credits(self):
|
264
|
+
"""Get credit transactions (positive amounts)."""
|
265
|
+
return self.filter(amount_usd__gt=0)
|
266
|
+
|
267
|
+
def debits(self):
|
268
|
+
"""Get debit transactions (negative amounts)."""
|
269
|
+
return self.filter(amount_usd__lt=0)
|
270
|
+
|
271
|
+
def large_amounts(self, threshold=100.0):
|
272
|
+
"""
|
273
|
+
Get transactions above threshold amount.
|
274
|
+
|
275
|
+
Args:
|
276
|
+
threshold: USD amount threshold (default: $100)
|
277
|
+
"""
|
278
|
+
return self.filter(amount_usd__gte=threshold)
|
279
|
+
|
280
|
+
def small_amounts(self, threshold=10.0):
|
281
|
+
"""
|
282
|
+
Get transactions below threshold amount.
|
283
|
+
|
284
|
+
Args:
|
285
|
+
threshold: USD amount threshold (default: $10)
|
286
|
+
"""
|
287
|
+
return self.filter(amount_usd__lte=threshold)
|
288
|
+
|
289
|
+
# Time-based filters
|
290
|
+
def recent(self, hours=24):
|
291
|
+
"""
|
292
|
+
Get transactions from last N hours.
|
293
|
+
|
294
|
+
Args:
|
295
|
+
hours: Number of hours to look back (default: 24)
|
296
|
+
"""
|
297
|
+
since = timezone.now() - timezone.timedelta(hours=hours)
|
298
|
+
return self.filter(created_at__gte=since)
|
299
|
+
|
300
|
+
def today(self):
|
301
|
+
"""Get transactions created today."""
|
302
|
+
today = timezone.now().date()
|
303
|
+
return self.filter(created_at__date=today)
|
304
|
+
|
305
|
+
def this_week(self):
|
306
|
+
"""Get transactions from this week."""
|
307
|
+
week_start = timezone.now().date() - timezone.timedelta(days=timezone.now().weekday())
|
308
|
+
return self.filter(created_at__date__gte=week_start)
|
309
|
+
|
310
|
+
def this_month(self):
|
311
|
+
"""Get transactions from this month."""
|
312
|
+
month_start = timezone.now().replace(day=1).date()
|
313
|
+
return self.filter(created_at__date__gte=month_start)
|
314
|
+
|
315
|
+
def date_range(self, start_date, end_date):
|
316
|
+
"""
|
317
|
+
Get transactions within date range.
|
318
|
+
|
319
|
+
Args:
|
320
|
+
start_date: Start date (inclusive)
|
321
|
+
end_date: End date (inclusive)
|
322
|
+
"""
|
323
|
+
return self.filter(created_at__date__range=[start_date, end_date])
|
324
|
+
|
325
|
+
# Aggregation methods
|
326
|
+
def total_amount(self):
|
327
|
+
"""Get total amount for queryset."""
|
328
|
+
result = self.aggregate(total=models.Sum('amount_usd'))
|
329
|
+
return result['total'] or 0.0
|
330
|
+
|
331
|
+
def total_credits(self):
|
332
|
+
"""Get total credit amount."""
|
333
|
+
result = self.credits().aggregate(total=models.Sum('amount_usd'))
|
334
|
+
return result['total'] or 0.0
|
335
|
+
|
336
|
+
def total_debits(self):
|
337
|
+
"""Get total debit amount (absolute value)."""
|
338
|
+
result = self.debits().aggregate(total=models.Sum('amount_usd'))
|
339
|
+
return abs(result['total'] or 0.0)
|
340
|
+
|
341
|
+
def average_amount(self):
|
342
|
+
"""Get average transaction amount."""
|
343
|
+
result = self.aggregate(avg=models.Avg('amount_usd'))
|
344
|
+
return result['avg'] or 0.0
|
345
|
+
|
346
|
+
def count_by_type(self):
|
347
|
+
"""Get count of transactions grouped by type."""
|
348
|
+
return self.values('transaction_type').annotate(
|
349
|
+
count=models.Count('id'),
|
350
|
+
total_amount=models.Sum('amount_usd')
|
351
|
+
).order_by('transaction_type')
|
352
|
+
|
353
|
+
def daily_summary(self, days=30):
|
354
|
+
"""
|
355
|
+
Get daily transaction summary for the last N days.
|
356
|
+
|
357
|
+
Args:
|
358
|
+
days: Number of days to analyze (default: 30)
|
359
|
+
"""
|
360
|
+
since = timezone.now().date() - timezone.timedelta(days=days)
|
361
|
+
return self.filter(created_at__date__gte=since).extra(
|
362
|
+
select={'day': 'date(created_at)'}
|
363
|
+
).values('day').annotate(
|
364
|
+
count=models.Count('id'),
|
365
|
+
total_amount=models.Sum('amount_usd'),
|
366
|
+
credits=models.Sum('amount_usd', filter=models.Q(amount_usd__gt=0)),
|
367
|
+
debits=models.Sum('amount_usd', filter=models.Q(amount_usd__lt=0))
|
368
|
+
).order_by('day')
|
369
|
+
|
370
|
+
|
371
|
+
class TransactionManager(models.Manager):
|
372
|
+
"""
|
373
|
+
Manager for transaction operations with optimized queries.
|
374
|
+
|
375
|
+
Provides high-level methods for transaction analysis and reporting.
|
376
|
+
"""
|
377
|
+
|
378
|
+
def get_queryset(self):
|
379
|
+
"""Return optimized queryset by default."""
|
380
|
+
return TransactionQuerySet(self.model, using=self._db)
|
381
|
+
|
382
|
+
def optimized(self):
|
383
|
+
"""Get optimized queryset."""
|
384
|
+
return self.get_queryset().optimized()
|
385
|
+
|
386
|
+
# User-based methods
|
387
|
+
def by_user(self, user):
|
388
|
+
"""Get transactions by user."""
|
389
|
+
return self.get_queryset().by_user(user)
|
390
|
+
|
391
|
+
def by_type(self, transaction_type):
|
392
|
+
"""Get transactions by type."""
|
393
|
+
return self.get_queryset().by_type(transaction_type)
|
394
|
+
|
395
|
+
# Transaction type methods
|
396
|
+
def deposits(self):
|
397
|
+
"""Get deposit transactions."""
|
398
|
+
return self.get_queryset().deposits()
|
399
|
+
|
400
|
+
def withdrawals(self):
|
401
|
+
"""Get withdrawal transactions."""
|
402
|
+
return self.get_queryset().withdrawals()
|
403
|
+
|
404
|
+
def payments(self):
|
405
|
+
"""Get payment transactions."""
|
406
|
+
return self.get_queryset().payments()
|
407
|
+
|
408
|
+
def refunds(self):
|
409
|
+
"""Get refund transactions."""
|
410
|
+
return self.get_queryset().refunds()
|
411
|
+
|
412
|
+
# Time-based methods
|
413
|
+
def recent(self, hours=24):
|
414
|
+
"""Get recent transactions."""
|
415
|
+
return self.get_queryset().recent(hours)
|
416
|
+
|
417
|
+
def today(self):
|
418
|
+
"""Get today's transactions."""
|
419
|
+
return self.get_queryset().today()
|
420
|
+
|
421
|
+
def this_week(self):
|
422
|
+
"""Get this week's transactions."""
|
423
|
+
return self.get_queryset().this_week()
|
424
|
+
|
425
|
+
def this_month(self):
|
426
|
+
"""Get this month's transactions."""
|
427
|
+
return self.get_queryset().this_month()
|
428
|
+
|
429
|
+
# Analysis methods
|
430
|
+
def get_user_balance_history(self, user, days=30):
|
431
|
+
"""
|
432
|
+
Get balance history for a user over the last N days.
|
433
|
+
|
434
|
+
Args:
|
435
|
+
user: User instance
|
436
|
+
days: Number of days to analyze (default: 30)
|
437
|
+
|
438
|
+
Returns:
|
439
|
+
list: Daily balance snapshots
|
440
|
+
"""
|
441
|
+
transactions = self.by_user(user).filter(
|
442
|
+
created_at__gte=timezone.now() - timezone.timedelta(days=days)
|
443
|
+
).order_by('created_at')
|
444
|
+
|
445
|
+
history = []
|
446
|
+
current_balance = 0.0
|
447
|
+
|
448
|
+
for transaction in transactions:
|
449
|
+
current_balance = transaction.balance_after
|
450
|
+
history.append({
|
451
|
+
'date': transaction.created_at.date(),
|
452
|
+
'balance': current_balance,
|
453
|
+
'transaction_id': str(transaction.id),
|
454
|
+
'transaction_type': transaction.transaction_type,
|
455
|
+
'amount': transaction.amount_usd
|
456
|
+
})
|
457
|
+
|
458
|
+
return history
|
459
|
+
|
460
|
+
def get_transaction_stats(self, user=None, days=30):
|
461
|
+
"""
|
462
|
+
Get transaction statistics.
|
463
|
+
|
464
|
+
Args:
|
465
|
+
user: User instance (optional, for user-specific stats)
|
466
|
+
days: Number of days to analyze (default: 30)
|
467
|
+
|
468
|
+
Returns:
|
469
|
+
dict: Transaction statistics
|
470
|
+
"""
|
471
|
+
queryset = self.get_queryset()
|
472
|
+
if user:
|
473
|
+
queryset = queryset.by_user(user)
|
474
|
+
|
475
|
+
since = timezone.now() - timezone.timedelta(days=days)
|
476
|
+
queryset = queryset.filter(created_at__gte=since)
|
477
|
+
|
478
|
+
stats = {
|
479
|
+
'total_transactions': queryset.count(),
|
480
|
+
'total_amount': queryset.total_amount(),
|
481
|
+
'total_credits': queryset.total_credits(),
|
482
|
+
'total_debits': queryset.total_debits(),
|
483
|
+
'average_amount': queryset.average_amount(),
|
484
|
+
'by_type': list(queryset.count_by_type()),
|
485
|
+
'deposits_count': queryset.deposits().count(),
|
486
|
+
'withdrawals_count': queryset.withdrawals().count(),
|
487
|
+
'payments_count': queryset.payments().count(),
|
488
|
+
'refunds_count': queryset.refunds().count(),
|
489
|
+
}
|
490
|
+
|
491
|
+
logger.info(f"Generated transaction stats for {days} days", extra={
|
492
|
+
'user_id': user.id if user else None,
|
493
|
+
'days': days,
|
494
|
+
'total_transactions': stats['total_transactions'],
|
495
|
+
'total_amount': stats['total_amount']
|
496
|
+
})
|
497
|
+
|
498
|
+
return stats
|
499
|
+
|
500
|
+
def get_daily_summary(self, days=30):
|
501
|
+
"""
|
502
|
+
Get daily transaction summary.
|
503
|
+
|
504
|
+
Args:
|
505
|
+
days: Number of days to analyze (default: 30)
|
506
|
+
|
507
|
+
Returns:
|
508
|
+
QuerySet: Daily summary data
|
509
|
+
"""
|
510
|
+
return self.get_queryset().daily_summary(days)
|
511
|
+
|
512
|
+
def create_deposit(self, user, amount, description=None, payment_id=None, metadata=None):
|
513
|
+
"""
|
514
|
+
Create a deposit transaction.
|
515
|
+
|
516
|
+
Args:
|
517
|
+
user: User instance
|
518
|
+
amount: Deposit amount (positive)
|
519
|
+
description: Transaction description
|
520
|
+
payment_id: Related payment ID
|
521
|
+
metadata: Additional metadata
|
522
|
+
|
523
|
+
Returns:
|
524
|
+
Transaction: Created transaction
|
525
|
+
"""
|
526
|
+
if amount <= 0:
|
527
|
+
raise ValueError("Deposit amount must be positive")
|
528
|
+
|
529
|
+
# Get or create user balance
|
530
|
+
from ..balance import UserBalance
|
531
|
+
balance = UserBalance.get_or_create_for_user(user)
|
532
|
+
|
533
|
+
# Create transaction via balance method (ensures atomicity)
|
534
|
+
transaction = balance.add_funds(
|
535
|
+
amount=amount,
|
536
|
+
transaction_type='deposit',
|
537
|
+
description=description or f"Deposit of ${amount:.2f}",
|
538
|
+
payment_id=payment_id
|
539
|
+
)
|
540
|
+
|
541
|
+
# Add metadata if provided
|
542
|
+
if metadata:
|
543
|
+
transaction.metadata = metadata
|
544
|
+
transaction.save(update_fields=['metadata'])
|
545
|
+
|
546
|
+
logger.info(f"Created deposit transaction", extra={
|
547
|
+
'user_id': user.id,
|
548
|
+
'amount': amount,
|
549
|
+
'transaction_id': str(transaction.id),
|
550
|
+
'payment_id': payment_id
|
551
|
+
})
|
552
|
+
|
553
|
+
return transaction
|
554
|
+
|
555
|
+
def create_withdrawal(self, user, amount, description=None, payment_id=None, metadata=None):
|
556
|
+
"""
|
557
|
+
Create a withdrawal transaction.
|
558
|
+
|
559
|
+
Args:
|
560
|
+
user: User instance
|
561
|
+
amount: Withdrawal amount (positive, will be made negative)
|
562
|
+
description: Transaction description
|
563
|
+
payment_id: Related payment ID
|
564
|
+
metadata: Additional metadata
|
565
|
+
|
566
|
+
Returns:
|
567
|
+
Transaction: Created transaction
|
568
|
+
"""
|
569
|
+
if amount <= 0:
|
570
|
+
raise ValueError("Withdrawal amount must be positive")
|
571
|
+
|
572
|
+
# Get user balance
|
573
|
+
from ..balance import UserBalance
|
574
|
+
try:
|
575
|
+
balance = UserBalance.objects.get(user=user)
|
576
|
+
except UserBalance.DoesNotExist:
|
577
|
+
raise ValueError("User has no balance record")
|
578
|
+
|
579
|
+
# Create transaction via balance method (ensures atomicity)
|
580
|
+
transaction = balance.subtract_funds(
|
581
|
+
amount=amount,
|
582
|
+
transaction_type='withdrawal',
|
583
|
+
description=description or f"Withdrawal of ${amount:.2f}",
|
584
|
+
payment_id=payment_id
|
585
|
+
)
|
586
|
+
|
587
|
+
# Add metadata if provided
|
588
|
+
if metadata:
|
589
|
+
transaction.metadata = metadata
|
590
|
+
transaction.save(update_fields=['metadata'])
|
591
|
+
|
592
|
+
logger.info(f"Created withdrawal transaction", extra={
|
593
|
+
'user_id': user.id,
|
594
|
+
'amount': amount,
|
595
|
+
'transaction_id': str(transaction.id),
|
596
|
+
'payment_id': payment_id
|
597
|
+
})
|
598
|
+
|
599
|
+
return transaction
|