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,342 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Basic billing utilities for production use.
|
3
|
-
|
4
|
-
Provides essential billing calculations and transaction management
|
5
|
-
without over-engineering.
|
6
|
-
"""
|
7
|
-
|
8
|
-
import logging
|
9
|
-
from typing import Dict, Any, Optional, Tuple
|
10
|
-
from decimal import Decimal, ROUND_HALF_UP
|
11
|
-
from datetime import datetime, timedelta
|
12
|
-
from django.utils import timezone
|
13
|
-
from django.db import transaction
|
14
|
-
from django.contrib.auth import get_user_model
|
15
|
-
|
16
|
-
from ..models import UserBalance, Transaction, Subscription
|
17
|
-
|
18
|
-
User = get_user_model()
|
19
|
-
logger = logging.getLogger(__name__)
|
20
|
-
|
21
|
-
|
22
|
-
def calculate_usage_cost(
|
23
|
-
subscription: Subscription,
|
24
|
-
usage_count: int,
|
25
|
-
billing_period: str = 'monthly'
|
26
|
-
) -> Decimal:
|
27
|
-
"""
|
28
|
-
Calculate cost for API usage.
|
29
|
-
|
30
|
-
Args:
|
31
|
-
subscription: User subscription
|
32
|
-
usage_count: Number of API calls
|
33
|
-
billing_period: Billing period (monthly/yearly)
|
34
|
-
|
35
|
-
Returns:
|
36
|
-
Cost in USD
|
37
|
-
"""
|
38
|
-
try:
|
39
|
-
endpoint_group = subscription.endpoint_group
|
40
|
-
|
41
|
-
# Get base price
|
42
|
-
if billing_period == 'monthly':
|
43
|
-
base_price = endpoint_group.monthly_price_usd
|
44
|
-
limit = endpoint_group.monthly_request_limit
|
45
|
-
else:
|
46
|
-
base_price = endpoint_group.yearly_price_usd
|
47
|
-
limit = endpoint_group.yearly_request_limit or (endpoint_group.monthly_request_limit * 12)
|
48
|
-
|
49
|
-
# If usage is within limit, cost is covered by subscription
|
50
|
-
if usage_count <= limit:
|
51
|
-
return Decimal('0.00')
|
52
|
-
|
53
|
-
# Calculate overage cost
|
54
|
-
overage = usage_count - limit
|
55
|
-
overage_rate = getattr(endpoint_group, 'overage_rate_per_request', Decimal('0.01'))
|
56
|
-
|
57
|
-
overage_cost = Decimal(overage) * overage_rate
|
58
|
-
|
59
|
-
# Round to 2 decimal places
|
60
|
-
return overage_cost.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
61
|
-
|
62
|
-
except Exception as e:
|
63
|
-
logger.error(f"Error calculating usage cost: {e}")
|
64
|
-
return Decimal('0.00')
|
65
|
-
|
66
|
-
|
67
|
-
def create_billing_transaction(
|
68
|
-
user: User,
|
69
|
-
amount: Decimal,
|
70
|
-
transaction_type: str,
|
71
|
-
source: str = 'billing',
|
72
|
-
description: Optional[str] = None,
|
73
|
-
reference_id: Optional[str] = None,
|
74
|
-
metadata: Optional[Dict[str, Any]] = None
|
75
|
-
) -> Tuple[bool, Optional[Transaction]]:
|
76
|
-
"""
|
77
|
-
Create a billing transaction with balance update.
|
78
|
-
|
79
|
-
Args:
|
80
|
-
user: User object
|
81
|
-
amount: Transaction amount (positive for credit, negative for debit)
|
82
|
-
transaction_type: Type of transaction
|
83
|
-
source: Source of transaction
|
84
|
-
description: Human-readable description
|
85
|
-
reference_id: External reference ID
|
86
|
-
metadata: Additional metadata
|
87
|
-
|
88
|
-
Returns:
|
89
|
-
Tuple of (success, transaction)
|
90
|
-
"""
|
91
|
-
try:
|
92
|
-
with transaction.atomic():
|
93
|
-
# Get or create user balance
|
94
|
-
balance, created = UserBalance.objects.get_or_create(
|
95
|
-
user=user,
|
96
|
-
currency_id=1, # Assuming USD currency has ID 1
|
97
|
-
defaults={
|
98
|
-
'available_amount': Decimal('0.00'),
|
99
|
-
'held_amount': Decimal('0.00')
|
100
|
-
}
|
101
|
-
)
|
102
|
-
|
103
|
-
# Check if debit is possible
|
104
|
-
if amount < 0 and not balance.can_debit(abs(amount)):
|
105
|
-
logger.warning(f"Insufficient balance for user {user.id}: {balance.available_amount} < {abs(amount)}")
|
106
|
-
return False, None
|
107
|
-
|
108
|
-
# Calculate new balance
|
109
|
-
old_balance = balance.available_amount
|
110
|
-
new_balance = old_balance + amount
|
111
|
-
|
112
|
-
# Update balance
|
113
|
-
balance.available_amount = new_balance
|
114
|
-
|
115
|
-
# Update totals
|
116
|
-
if amount > 0:
|
117
|
-
balance.total_earned += amount
|
118
|
-
else:
|
119
|
-
balance.total_spent += abs(amount)
|
120
|
-
|
121
|
-
balance.save()
|
122
|
-
|
123
|
-
# Create transaction record
|
124
|
-
txn = Transaction.objects.create(
|
125
|
-
user=user,
|
126
|
-
balance=balance,
|
127
|
-
transaction_type=transaction_type,
|
128
|
-
amount=amount,
|
129
|
-
balance_before=old_balance,
|
130
|
-
balance_after=new_balance,
|
131
|
-
source=source,
|
132
|
-
description=description or f"{transaction_type} transaction",
|
133
|
-
reference_id=reference_id,
|
134
|
-
metadata=metadata or {}
|
135
|
-
)
|
136
|
-
|
137
|
-
logger.info(f"Created billing transaction: {txn.id} for user {user.id}, amount: {amount}")
|
138
|
-
return True, txn
|
139
|
-
|
140
|
-
except Exception as e:
|
141
|
-
logger.error(f"Error creating billing transaction for user {user.id}: {e}")
|
142
|
-
return False, None
|
143
|
-
|
144
|
-
|
145
|
-
def calculate_subscription_refund(
|
146
|
-
subscription: Subscription,
|
147
|
-
refund_strategy: str = 'prorated',
|
148
|
-
cancellation_date: Optional[datetime] = None
|
149
|
-
) -> Dict[str, Any]:
|
150
|
-
"""
|
151
|
-
Calculate refund amount for cancelled subscription.
|
152
|
-
|
153
|
-
Args:
|
154
|
-
subscription: Subscription to refund
|
155
|
-
refund_strategy: 'prorated', 'full', or 'none'
|
156
|
-
cancellation_date: Date of cancellation (defaults to now)
|
157
|
-
|
158
|
-
Returns:
|
159
|
-
Dict with refund calculation details
|
160
|
-
"""
|
161
|
-
try:
|
162
|
-
if not cancellation_date:
|
163
|
-
cancellation_date = timezone.now()
|
164
|
-
|
165
|
-
# Get subscription details
|
166
|
-
start_date = subscription.starts_at
|
167
|
-
end_date = subscription.expires_at
|
168
|
-
|
169
|
-
if subscription.billing_period == 'monthly':
|
170
|
-
original_amount = subscription.endpoint_group.monthly_price_usd
|
171
|
-
else:
|
172
|
-
original_amount = subscription.endpoint_group.yearly_price_usd
|
173
|
-
|
174
|
-
# Calculate refund based on strategy
|
175
|
-
if refund_strategy == 'none':
|
176
|
-
refund_amount = Decimal('0.00')
|
177
|
-
refund_reason = "No refund policy"
|
178
|
-
|
179
|
-
elif refund_strategy == 'full':
|
180
|
-
refund_amount = original_amount
|
181
|
-
refund_reason = "Full refund"
|
182
|
-
|
183
|
-
elif refund_strategy == 'prorated':
|
184
|
-
# Calculate prorated refund
|
185
|
-
total_days = (end_date - start_date).days
|
186
|
-
used_days = (cancellation_date - start_date).days
|
187
|
-
remaining_days = max(0, total_days - used_days)
|
188
|
-
|
189
|
-
if total_days > 0:
|
190
|
-
refund_percentage = Decimal(remaining_days) / Decimal(total_days)
|
191
|
-
refund_amount = (original_amount * refund_percentage).quantize(
|
192
|
-
Decimal('0.01'), rounding=ROUND_HALF_UP
|
193
|
-
)
|
194
|
-
else:
|
195
|
-
refund_amount = Decimal('0.00')
|
196
|
-
|
197
|
-
refund_reason = f"Prorated refund: {remaining_days}/{total_days} days remaining"
|
198
|
-
|
199
|
-
else:
|
200
|
-
refund_amount = Decimal('0.00')
|
201
|
-
refund_reason = "Unknown refund strategy"
|
202
|
-
|
203
|
-
return {
|
204
|
-
'refund_amount': refund_amount,
|
205
|
-
'original_amount': original_amount,
|
206
|
-
'refund_strategy': refund_strategy,
|
207
|
-
'refund_reason': refund_reason,
|
208
|
-
'calculation_date': cancellation_date.isoformat(),
|
209
|
-
'subscription_id': str(subscription.id),
|
210
|
-
'billing_period': subscription.billing_period
|
211
|
-
}
|
212
|
-
|
213
|
-
except Exception as e:
|
214
|
-
logger.error(f"Error calculating refund for subscription {subscription.id}: {e}")
|
215
|
-
return {
|
216
|
-
'refund_amount': Decimal('0.00'),
|
217
|
-
'original_amount': Decimal('0.00'),
|
218
|
-
'refund_strategy': refund_strategy,
|
219
|
-
'refund_reason': f"Calculation error: {str(e)}",
|
220
|
-
'error': True
|
221
|
-
}
|
222
|
-
|
223
|
-
|
224
|
-
def process_subscription_billing(subscription: Subscription) -> Dict[str, Any]:
|
225
|
-
"""
|
226
|
-
Process billing for subscription renewal.
|
227
|
-
|
228
|
-
Args:
|
229
|
-
subscription: Subscription to bill
|
230
|
-
|
231
|
-
Returns:
|
232
|
-
Dict with billing results
|
233
|
-
"""
|
234
|
-
try:
|
235
|
-
# Calculate billing amount
|
236
|
-
if subscription.billing_period == 'monthly':
|
237
|
-
amount = subscription.endpoint_group.monthly_price_usd
|
238
|
-
billing_period_days = 30
|
239
|
-
else:
|
240
|
-
amount = subscription.endpoint_group.yearly_price_usd
|
241
|
-
billing_period_days = 365
|
242
|
-
|
243
|
-
# Create billing transaction
|
244
|
-
success, txn = create_billing_transaction(
|
245
|
-
user=subscription.user,
|
246
|
-
amount=-amount, # Negative for debit
|
247
|
-
transaction_type='subscription_billing',
|
248
|
-
source='subscription_renewal',
|
249
|
-
description=f"Subscription renewal: {subscription.endpoint_group.display_name}",
|
250
|
-
reference_id=str(subscription.id),
|
251
|
-
metadata={
|
252
|
-
'subscription_id': str(subscription.id),
|
253
|
-
'billing_period': subscription.billing_period,
|
254
|
-
'endpoint_group': subscription.endpoint_group.name
|
255
|
-
}
|
256
|
-
)
|
257
|
-
|
258
|
-
if success:
|
259
|
-
# Update subscription
|
260
|
-
subscription.next_billing_at = timezone.now() + timedelta(days=billing_period_days)
|
261
|
-
subscription.current_usage = 0 # Reset usage
|
262
|
-
subscription.save()
|
263
|
-
|
264
|
-
logger.info(f"Successfully billed subscription {subscription.id} for ${amount}")
|
265
|
-
|
266
|
-
return {
|
267
|
-
'success': True,
|
268
|
-
'amount_billed': amount,
|
269
|
-
'transaction_id': str(txn.id),
|
270
|
-
'next_billing_at': subscription.next_billing_at.isoformat()
|
271
|
-
}
|
272
|
-
else:
|
273
|
-
logger.warning(f"Failed to bill subscription {subscription.id}: insufficient balance")
|
274
|
-
|
275
|
-
return {
|
276
|
-
'success': False,
|
277
|
-
'error': 'Insufficient balance',
|
278
|
-
'amount_required': amount,
|
279
|
-
'user_balance': UserBalance.objects.get(user=subscription.user).available_amount
|
280
|
-
}
|
281
|
-
|
282
|
-
except Exception as e:
|
283
|
-
logger.error(f"Error processing subscription billing {subscription.id}: {e}")
|
284
|
-
return {
|
285
|
-
'success': False,
|
286
|
-
'error': str(e)
|
287
|
-
}
|
288
|
-
|
289
|
-
|
290
|
-
def get_billing_summary(user: User, days: int = 30) -> Dict[str, Any]:
|
291
|
-
"""
|
292
|
-
Get billing summary for user over specified period.
|
293
|
-
|
294
|
-
Args:
|
295
|
-
user: User object
|
296
|
-
days: Number of days to include
|
297
|
-
|
298
|
-
Returns:
|
299
|
-
Dict with billing summary
|
300
|
-
"""
|
301
|
-
try:
|
302
|
-
cutoff_date = timezone.now() - timedelta(days=days)
|
303
|
-
|
304
|
-
# Get transactions
|
305
|
-
transactions = Transaction.objects.filter(
|
306
|
-
user=user,
|
307
|
-
created_at__gte=cutoff_date
|
308
|
-
)
|
309
|
-
|
310
|
-
# Calculate totals
|
311
|
-
from django.db import models
|
312
|
-
|
313
|
-
total_credits = transactions.filter(amount__gt=0).aggregate(
|
314
|
-
total=models.Sum('amount')
|
315
|
-
)['total'] or Decimal('0.00')
|
316
|
-
|
317
|
-
total_debits = transactions.filter(amount__lt=0).aggregate(
|
318
|
-
total=models.Sum('amount')
|
319
|
-
)['total'] or Decimal('0.00')
|
320
|
-
|
321
|
-
# Get current balance
|
322
|
-
try:
|
323
|
-
balance = UserBalance.objects.get(user=user)
|
324
|
-
current_balance = balance.available_amount
|
325
|
-
except UserBalance.DoesNotExist:
|
326
|
-
current_balance = Decimal('0.00')
|
327
|
-
|
328
|
-
return {
|
329
|
-
'period_days': days,
|
330
|
-
'total_credits': total_credits,
|
331
|
-
'total_debits': abs(total_debits),
|
332
|
-
'net_change': total_credits + total_debits, # total_debits is negative
|
333
|
-
'current_balance': current_balance,
|
334
|
-
'transaction_count': transactions.count()
|
335
|
-
}
|
336
|
-
|
337
|
-
except Exception as e:
|
338
|
-
logger.error(f"Error getting billing summary for user {user.id}: {e}")
|
339
|
-
return {
|
340
|
-
'error': str(e),
|
341
|
-
'period_days': days
|
342
|
-
}
|
@@ -1,245 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Configuration utilities for payments module.
|
3
|
-
|
4
|
-
Universal utilities for working with django-cfg settings and configuration.
|
5
|
-
"""
|
6
|
-
|
7
|
-
import logging
|
8
|
-
from typing import Optional, Dict, Any, Type
|
9
|
-
from django.conf import settings
|
10
|
-
|
11
|
-
from django_cfg.modules.base import BaseCfgModule
|
12
|
-
from ..config.settings import PaymentsSettings
|
13
|
-
|
14
|
-
logger = logging.getLogger(__name__)
|
15
|
-
|
16
|
-
|
17
|
-
class PaymentsConfigMixin:
|
18
|
-
"""Mixin for accessing payments configuration through django-cfg."""
|
19
|
-
|
20
|
-
_payments_config_cache: Optional[PaymentsSettings] = None
|
21
|
-
_config_module: Optional[BaseCfgModule] = None
|
22
|
-
|
23
|
-
@classmethod
|
24
|
-
def get_payments_config(cls) -> PaymentsSettings:
|
25
|
-
"""Get payments configuration from django-cfg."""
|
26
|
-
if cls._payments_config_cache is None:
|
27
|
-
cls._payments_config_cache = cls._load_payments_config()
|
28
|
-
return cls._payments_config_cache
|
29
|
-
|
30
|
-
@classmethod
|
31
|
-
def _load_payments_config(cls) -> PaymentsSettings:
|
32
|
-
"""Load payments configuration using BaseCfgModule."""
|
33
|
-
try:
|
34
|
-
if cls._config_module is None:
|
35
|
-
from ..config.module import PaymentsCfgModule
|
36
|
-
cls._config_module = PaymentsCfgModule()
|
37
|
-
|
38
|
-
return cls._config_module.get_config()
|
39
|
-
except Exception as e:
|
40
|
-
logger.warning(f"Failed to load payments config: {e}")
|
41
|
-
return PaymentsSettings()
|
42
|
-
|
43
|
-
@classmethod
|
44
|
-
def reset_config_cache(cls):
|
45
|
-
"""Reset configuration cache."""
|
46
|
-
cls._payments_config_cache = None
|
47
|
-
if cls._config_module:
|
48
|
-
cls._config_module.reset_cache()
|
49
|
-
|
50
|
-
|
51
|
-
class RedisConfigHelper(PaymentsConfigMixin):
|
52
|
-
"""Helper for Redis configuration."""
|
53
|
-
|
54
|
-
@classmethod
|
55
|
-
def get_redis_config(cls) -> Dict[str, Any]:
|
56
|
-
"""Get Redis configuration for payments."""
|
57
|
-
config = cls.get_payments_config()
|
58
|
-
|
59
|
-
# Default Redis settings
|
60
|
-
redis_config = {
|
61
|
-
'host': 'localhost',
|
62
|
-
'port': 6379,
|
63
|
-
'db': 0,
|
64
|
-
'decode_responses': True,
|
65
|
-
'socket_timeout': 5,
|
66
|
-
'socket_connect_timeout': 5,
|
67
|
-
'retry_on_timeout': True,
|
68
|
-
'health_check_interval': 30,
|
69
|
-
}
|
70
|
-
|
71
|
-
# Try to get Redis settings from Django CACHES
|
72
|
-
django_cache = getattr(settings, 'CACHES', {}).get('default', {})
|
73
|
-
if 'redis' in django_cache.get('BACKEND', '').lower():
|
74
|
-
location = django_cache.get('LOCATION', '')
|
75
|
-
if location.startswith('redis://'):
|
76
|
-
# Parse redis://host:port/db format
|
77
|
-
try:
|
78
|
-
# Simple parsing for redis://host:port/db
|
79
|
-
parts = location.replace('redis://', '').split('/')
|
80
|
-
host_port = parts[0].split(':')
|
81
|
-
redis_config['host'] = host_port[0]
|
82
|
-
if len(host_port) > 1:
|
83
|
-
redis_config['port'] = int(host_port[1])
|
84
|
-
if len(parts) > 1:
|
85
|
-
redis_config['db'] = int(parts[1])
|
86
|
-
except (ValueError, IndexError) as e:
|
87
|
-
logger.warning(f"Failed to parse Redis URL {location}: {e}")
|
88
|
-
|
89
|
-
# Override with payments-specific Redis config if available
|
90
|
-
if hasattr(config, 'redis') and config.redis:
|
91
|
-
redis_config.update(config.redis.dict())
|
92
|
-
|
93
|
-
return redis_config
|
94
|
-
|
95
|
-
@classmethod
|
96
|
-
def is_redis_available(cls) -> bool:
|
97
|
-
"""Check if Redis is available and configured."""
|
98
|
-
try:
|
99
|
-
import redis
|
100
|
-
config = cls.get_redis_config()
|
101
|
-
client = redis.Redis(**config)
|
102
|
-
client.ping()
|
103
|
-
return True
|
104
|
-
except Exception as e:
|
105
|
-
logger.debug(f"Redis not available: {e}")
|
106
|
-
return False
|
107
|
-
|
108
|
-
|
109
|
-
class CacheConfigHelper(PaymentsConfigMixin):
|
110
|
-
"""Helper for cache configuration."""
|
111
|
-
|
112
|
-
@classmethod
|
113
|
-
def get_cache_backend_type(cls) -> str:
|
114
|
-
"""Get Django cache backend type."""
|
115
|
-
django_cache = getattr(settings, 'CACHES', {}).get('default', {})
|
116
|
-
backend = django_cache.get('BACKEND', '').lower()
|
117
|
-
|
118
|
-
if 'redis' in backend:
|
119
|
-
return 'redis'
|
120
|
-
elif 'memcached' in backend:
|
121
|
-
return 'memcached'
|
122
|
-
elif 'database' in backend:
|
123
|
-
return 'database'
|
124
|
-
elif 'dummy' in backend:
|
125
|
-
return 'dummy'
|
126
|
-
else:
|
127
|
-
return 'unknown'
|
128
|
-
|
129
|
-
@classmethod
|
130
|
-
def is_cache_enabled(cls) -> bool:
|
131
|
-
"""Check if cache is properly configured (not dummy)."""
|
132
|
-
return cls.get_cache_backend_type() != 'dummy'
|
133
|
-
|
134
|
-
@classmethod
|
135
|
-
def get_cache_timeout(cls, operation: str) -> int:
|
136
|
-
"""Get cache timeout for specific operation."""
|
137
|
-
config = cls.get_payments_config()
|
138
|
-
|
139
|
-
timeouts = {
|
140
|
-
'api_key': 300, # 5 minutes
|
141
|
-
'rate_limit': 3600, # 1 hour
|
142
|
-
'session': 1800, # 30 minutes
|
143
|
-
'default': 600 # 10 minutes
|
144
|
-
}
|
145
|
-
|
146
|
-
# Override with config if available
|
147
|
-
if hasattr(config, 'cache_timeouts') and config.cache_timeouts:
|
148
|
-
timeouts.update(config.cache_timeouts)
|
149
|
-
|
150
|
-
return timeouts.get(operation, timeouts['default'])
|
151
|
-
|
152
|
-
|
153
|
-
class ProviderConfigHelper(PaymentsConfigMixin):
|
154
|
-
"""Helper for payment provider configuration."""
|
155
|
-
|
156
|
-
@classmethod
|
157
|
-
def get_enabled_providers(cls) -> list:
|
158
|
-
"""Get list of enabled payment providers."""
|
159
|
-
config = cls.get_payments_config()
|
160
|
-
if not config.enabled:
|
161
|
-
return []
|
162
|
-
|
163
|
-
enabled = []
|
164
|
-
if hasattr(config, 'providers') and config.providers:
|
165
|
-
for provider_name, provider_config in config.providers.items():
|
166
|
-
if provider_config and cls._is_provider_properly_configured(provider_name, provider_config):
|
167
|
-
enabled.append(provider_name)
|
168
|
-
|
169
|
-
return enabled
|
170
|
-
|
171
|
-
@classmethod
|
172
|
-
def get_provider_config(cls, provider_name: str) -> Optional[Any]:
|
173
|
-
"""Get configuration for specific provider."""
|
174
|
-
config = cls.get_payments_config()
|
175
|
-
if not config.enabled or not hasattr(config, 'providers'):
|
176
|
-
return None
|
177
|
-
|
178
|
-
return config.providers.get(provider_name)
|
179
|
-
|
180
|
-
@classmethod
|
181
|
-
def is_provider_enabled(cls, provider_name: str) -> bool:
|
182
|
-
"""Check if specific provider is enabled and configured."""
|
183
|
-
return provider_name in cls.get_enabled_providers()
|
184
|
-
|
185
|
-
@classmethod
|
186
|
-
def _is_provider_properly_configured(cls, provider_name: str, provider_config: Any) -> bool:
|
187
|
-
"""Check if provider configuration is complete."""
|
188
|
-
if not provider_config:
|
189
|
-
return False
|
190
|
-
|
191
|
-
# Basic validation - each provider should have api_key
|
192
|
-
if not hasattr(provider_config, 'api_key') or not provider_config.api_key:
|
193
|
-
return False
|
194
|
-
|
195
|
-
# Provider-specific validations
|
196
|
-
if provider_name == 'nowpayments':
|
197
|
-
return True # api_key is sufficient
|
198
|
-
elif provider_name == 'stripe':
|
199
|
-
return True # api_key is sufficient
|
200
|
-
elif provider_name == 'cryptapi':
|
201
|
-
return hasattr(provider_config, 'own_address') and provider_config.own_address
|
202
|
-
elif provider_name == 'cryptomus':
|
203
|
-
return hasattr(provider_config, 'merchant_uuid') and provider_config.merchant_uuid
|
204
|
-
|
205
|
-
return True
|
206
|
-
|
207
|
-
|
208
|
-
class PaymentsConfigUtil:
|
209
|
-
"""
|
210
|
-
Universal utility for payments configuration.
|
211
|
-
|
212
|
-
Combines all config helpers into one convenient interface.
|
213
|
-
"""
|
214
|
-
|
215
|
-
redis = RedisConfigHelper
|
216
|
-
cache = CacheConfigHelper
|
217
|
-
providers = ProviderConfigHelper
|
218
|
-
|
219
|
-
@staticmethod
|
220
|
-
def get_config() -> PaymentsSettings:
|
221
|
-
"""Get payments configuration."""
|
222
|
-
return PaymentsConfigMixin.get_payments_config()
|
223
|
-
|
224
|
-
@staticmethod
|
225
|
-
def is_payments_enabled() -> bool:
|
226
|
-
"""Check if payments module is enabled."""
|
227
|
-
config = PaymentsConfigMixin.get_payments_config()
|
228
|
-
return config.enabled
|
229
|
-
|
230
|
-
@staticmethod
|
231
|
-
def is_debug_mode() -> bool:
|
232
|
-
"""Check if payments module is in debug mode."""
|
233
|
-
config = PaymentsConfigMixin.get_payments_config()
|
234
|
-
return getattr(config, 'debug_mode', False)
|
235
|
-
|
236
|
-
@staticmethod
|
237
|
-
def reset_all_caches():
|
238
|
-
"""Reset all configuration caches."""
|
239
|
-
PaymentsConfigMixin.reset_config_cache()
|
240
|
-
|
241
|
-
|
242
|
-
# Convenience exports
|
243
|
-
get_payments_config = PaymentsConfigUtil.get_config
|
244
|
-
is_payments_enabled = PaymentsConfigUtil.is_payments_enabled
|
245
|
-
is_debug_mode = PaymentsConfigUtil.is_debug_mode
|