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
@@ -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
|
-
]
|