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,641 @@
|
|
1
|
+
"""
|
2
|
+
Subscription managers for the Universal Payment System v2.0.
|
3
|
+
|
4
|
+
Optimized querysets and managers for subscription and endpoint group operations.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from django.db import models
|
8
|
+
from django.utils import timezone
|
9
|
+
from datetime import timedelta
|
10
|
+
from django_cfg.modules.django_logger import get_logger
|
11
|
+
|
12
|
+
logger = get_logger("subscription_managers")
|
13
|
+
|
14
|
+
|
15
|
+
class SubscriptionQuerySet(models.QuerySet):
|
16
|
+
"""
|
17
|
+
Optimized queryset for subscription operations.
|
18
|
+
|
19
|
+
Provides efficient queries for subscription management and access control.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def optimized(self):
|
23
|
+
"""Prevent N+1 queries with select_related and prefetch_related."""
|
24
|
+
return self.select_related('user').prefetch_related('endpoint_groups')
|
25
|
+
|
26
|
+
def by_user(self, user):
|
27
|
+
"""Filter subscriptions by user."""
|
28
|
+
return self.filter(user=user)
|
29
|
+
|
30
|
+
def by_tier(self, tier):
|
31
|
+
"""Filter by subscription tier."""
|
32
|
+
return self.filter(tier=tier)
|
33
|
+
|
34
|
+
def by_status(self, status):
|
35
|
+
"""Filter by subscription status."""
|
36
|
+
return self.filter(status=status)
|
37
|
+
|
38
|
+
# Status-based filters
|
39
|
+
def active(self):
|
40
|
+
"""
|
41
|
+
Get active subscriptions that are not expired.
|
42
|
+
|
43
|
+
Returns subscriptions with status='active' and expires_at > now.
|
44
|
+
"""
|
45
|
+
return self.filter(
|
46
|
+
status='active',
|
47
|
+
expires_at__gt=timezone.now()
|
48
|
+
)
|
49
|
+
|
50
|
+
def inactive(self):
|
51
|
+
"""Get inactive subscriptions."""
|
52
|
+
return self.filter(status='inactive')
|
53
|
+
|
54
|
+
def suspended(self):
|
55
|
+
"""Get suspended subscriptions."""
|
56
|
+
return self.filter(status='suspended')
|
57
|
+
|
58
|
+
def cancelled(self):
|
59
|
+
"""Get cancelled subscriptions."""
|
60
|
+
return self.filter(status='cancelled')
|
61
|
+
|
62
|
+
def expired(self):
|
63
|
+
"""
|
64
|
+
Get expired subscriptions.
|
65
|
+
|
66
|
+
Returns subscriptions where expires_at <= now, regardless of status.
|
67
|
+
"""
|
68
|
+
return self.filter(expires_at__lte=timezone.now())
|
69
|
+
|
70
|
+
def expiring_soon(self, days=7):
|
71
|
+
"""
|
72
|
+
Get subscriptions expiring in the next N days.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
days: Number of days to look ahead (default: 7)
|
76
|
+
"""
|
77
|
+
soon = timezone.now() + timedelta(days=days)
|
78
|
+
return self.filter(
|
79
|
+
expires_at__lte=soon,
|
80
|
+
expires_at__gt=timezone.now(),
|
81
|
+
status='active'
|
82
|
+
)
|
83
|
+
|
84
|
+
# Tier-based filters
|
85
|
+
def free_tier(self):
|
86
|
+
"""Get free tier subscriptions."""
|
87
|
+
return self.filter(tier='free')
|
88
|
+
|
89
|
+
def basic_tier(self):
|
90
|
+
"""Get basic tier subscriptions."""
|
91
|
+
return self.filter(tier='basic')
|
92
|
+
|
93
|
+
def pro_tier(self):
|
94
|
+
"""Get pro tier subscriptions."""
|
95
|
+
return self.filter(tier='pro')
|
96
|
+
|
97
|
+
def enterprise_tier(self):
|
98
|
+
"""Get enterprise tier subscriptions."""
|
99
|
+
return self.filter(tier='enterprise')
|
100
|
+
|
101
|
+
def paid_tiers(self):
|
102
|
+
"""Get paid tier subscriptions (non-free)."""
|
103
|
+
return self.exclude(tier='free')
|
104
|
+
|
105
|
+
# Time-based filters
|
106
|
+
def created_recently(self, days=30):
|
107
|
+
"""
|
108
|
+
Get subscriptions created in the last N days.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
days: Number of days to look back (default: 30)
|
112
|
+
"""
|
113
|
+
since = timezone.now() - timedelta(days=days)
|
114
|
+
return self.filter(created_at__gte=since)
|
115
|
+
|
116
|
+
def renewed_recently(self, days=30):
|
117
|
+
"""
|
118
|
+
Get subscriptions renewed in the last N days.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
days: Number of days to look back (default: 30)
|
122
|
+
"""
|
123
|
+
since = timezone.now() - timedelta(days=days)
|
124
|
+
return self.filter(updated_at__gte=since, status='active')
|
125
|
+
|
126
|
+
# Usage-based filters
|
127
|
+
def with_usage(self):
|
128
|
+
"""Get subscriptions that have been used (total_requests > 0)."""
|
129
|
+
return self.filter(total_requests__gt=0)
|
130
|
+
|
131
|
+
def without_usage(self):
|
132
|
+
"""Get subscriptions that have never been used."""
|
133
|
+
return self.filter(total_requests=0)
|
134
|
+
|
135
|
+
def high_usage(self, threshold=1000):
|
136
|
+
"""
|
137
|
+
Get subscriptions with high usage.
|
138
|
+
|
139
|
+
Args:
|
140
|
+
threshold: Request count threshold (default: 1000)
|
141
|
+
"""
|
142
|
+
return self.filter(total_requests__gte=threshold)
|
143
|
+
|
144
|
+
def recent_usage(self, hours=24):
|
145
|
+
"""
|
146
|
+
Get subscriptions used in the last N hours.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
hours: Number of hours to look back (default: 24)
|
150
|
+
"""
|
151
|
+
since = timezone.now() - timedelta(hours=hours)
|
152
|
+
return self.filter(last_request_at__gte=since)
|
153
|
+
|
154
|
+
# Auto-renewal filters
|
155
|
+
def auto_renewing(self):
|
156
|
+
"""Get subscriptions with auto-renewal enabled."""
|
157
|
+
return self.filter(auto_renew=True)
|
158
|
+
|
159
|
+
def manual_renewal(self):
|
160
|
+
"""Get subscriptions with manual renewal."""
|
161
|
+
return self.filter(auto_renew=False)
|
162
|
+
|
163
|
+
# Endpoint access filters
|
164
|
+
def with_endpoint_access(self, endpoint_group_code):
|
165
|
+
"""
|
166
|
+
Get subscriptions with access to specific endpoint group.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
endpoint_group_code: Endpoint group code to check
|
170
|
+
"""
|
171
|
+
return self.filter(
|
172
|
+
endpoint_groups__code=endpoint_group_code,
|
173
|
+
endpoint_groups__is_enabled=True
|
174
|
+
)
|
175
|
+
|
176
|
+
# Aggregation methods
|
177
|
+
def total_revenue(self):
|
178
|
+
"""Get total monthly revenue from active subscriptions."""
|
179
|
+
result = self.active().aggregate(total=models.Sum('monthly_cost_usd'))
|
180
|
+
return result['total'] or 0.0
|
181
|
+
|
182
|
+
def average_cost(self):
|
183
|
+
"""Get average monthly cost."""
|
184
|
+
result = self.aggregate(avg=models.Avg('monthly_cost_usd'))
|
185
|
+
return result['avg'] or 0.0
|
186
|
+
|
187
|
+
def count_by_tier(self):
|
188
|
+
"""Get count of subscriptions grouped by tier."""
|
189
|
+
return self.values('tier').annotate(count=models.Count('id')).order_by('tier')
|
190
|
+
|
191
|
+
def count_by_status(self):
|
192
|
+
"""Get count of subscriptions grouped by status."""
|
193
|
+
return self.values('status').annotate(count=models.Count('id')).order_by('status')
|
194
|
+
|
195
|
+
def usage_stats(self):
|
196
|
+
"""Get usage statistics."""
|
197
|
+
return self.aggregate(
|
198
|
+
total_requests=models.Sum('total_requests'),
|
199
|
+
avg_requests=models.Avg('total_requests'),
|
200
|
+
max_requests=models.Max('total_requests'),
|
201
|
+
active_users=models.Count('user', distinct=True)
|
202
|
+
)
|
203
|
+
|
204
|
+
|
205
|
+
class SubscriptionManager(models.Manager):
|
206
|
+
"""
|
207
|
+
Manager for subscription operations with business logic.
|
208
|
+
|
209
|
+
Provides high-level methods for subscription management and access control.
|
210
|
+
"""
|
211
|
+
|
212
|
+
def get_queryset(self):
|
213
|
+
"""Return optimized queryset by default."""
|
214
|
+
return SubscriptionQuerySet(self.model, using=self._db)
|
215
|
+
|
216
|
+
def optimized(self):
|
217
|
+
"""Get optimized queryset."""
|
218
|
+
return self.get_queryset().optimized()
|
219
|
+
|
220
|
+
# Status-based methods
|
221
|
+
def active(self):
|
222
|
+
"""Get active subscriptions."""
|
223
|
+
return self.get_queryset().active()
|
224
|
+
|
225
|
+
def expired(self):
|
226
|
+
"""Get expired subscriptions."""
|
227
|
+
return self.get_queryset().expired()
|
228
|
+
|
229
|
+
def expiring_soon(self, days=7):
|
230
|
+
"""Get subscriptions expiring soon."""
|
231
|
+
return self.get_queryset().expiring_soon(days)
|
232
|
+
|
233
|
+
# Tier-based methods
|
234
|
+
def by_tier(self, tier):
|
235
|
+
"""Get subscriptions by tier."""
|
236
|
+
return self.get_queryset().by_tier(tier)
|
237
|
+
|
238
|
+
def free_tier(self):
|
239
|
+
"""Get free tier subscriptions."""
|
240
|
+
return self.get_queryset().free_tier()
|
241
|
+
|
242
|
+
def paid_tiers(self):
|
243
|
+
"""Get paid tier subscriptions."""
|
244
|
+
return self.get_queryset().paid_tiers()
|
245
|
+
|
246
|
+
# User methods
|
247
|
+
def get_active_for_user(self, user):
|
248
|
+
"""
|
249
|
+
Get active subscription for user.
|
250
|
+
|
251
|
+
Args:
|
252
|
+
user: User instance
|
253
|
+
|
254
|
+
Returns:
|
255
|
+
Subscription or None: Active subscription if exists
|
256
|
+
"""
|
257
|
+
try:
|
258
|
+
return self.active().get(user=user)
|
259
|
+
except self.model.DoesNotExist:
|
260
|
+
return None
|
261
|
+
|
262
|
+
def has_active_subscription(self, user):
|
263
|
+
"""
|
264
|
+
Check if user has an active subscription.
|
265
|
+
|
266
|
+
Args:
|
267
|
+
user: User instance
|
268
|
+
|
269
|
+
Returns:
|
270
|
+
bool: True if user has active subscription
|
271
|
+
"""
|
272
|
+
return self.active().filter(user=user).exists()
|
273
|
+
|
274
|
+
def get_or_create_free_subscription(self, user):
|
275
|
+
"""
|
276
|
+
Get existing subscription or create free tier subscription for user.
|
277
|
+
|
278
|
+
Args:
|
279
|
+
user: User instance
|
280
|
+
|
281
|
+
Returns:
|
282
|
+
tuple: (Subscription, created)
|
283
|
+
"""
|
284
|
+
# Check for existing active subscription
|
285
|
+
existing = self.get_active_for_user(user)
|
286
|
+
if existing:
|
287
|
+
return existing, False
|
288
|
+
|
289
|
+
# Create free subscription
|
290
|
+
subscription = self.model.create_free_subscription(user)
|
291
|
+
|
292
|
+
logger.info(f"Created free subscription for user", extra={
|
293
|
+
'user_id': user.id,
|
294
|
+
'subscription_id': str(subscription.id)
|
295
|
+
})
|
296
|
+
|
297
|
+
return subscription, True
|
298
|
+
|
299
|
+
# Access control methods
|
300
|
+
def check_endpoint_access(self, user, endpoint_group_code):
|
301
|
+
"""
|
302
|
+
Check if user has access to specific endpoint group.
|
303
|
+
|
304
|
+
Args:
|
305
|
+
user: User instance
|
306
|
+
endpoint_group_code: Endpoint group code to check
|
307
|
+
|
308
|
+
Returns:
|
309
|
+
bool: True if user has access
|
310
|
+
"""
|
311
|
+
subscription = self.get_active_for_user(user)
|
312
|
+
if not subscription:
|
313
|
+
return False
|
314
|
+
|
315
|
+
return subscription.has_access_to_endpoint_group(endpoint_group_code)
|
316
|
+
|
317
|
+
def get_user_rate_limits(self, user):
|
318
|
+
"""
|
319
|
+
Get rate limits for user based on their subscription.
|
320
|
+
|
321
|
+
Args:
|
322
|
+
user: User instance
|
323
|
+
|
324
|
+
Returns:
|
325
|
+
dict: Rate limit information
|
326
|
+
"""
|
327
|
+
subscription = self.get_active_for_user(user)
|
328
|
+
if not subscription:
|
329
|
+
return {
|
330
|
+
'requests_per_hour': 0,
|
331
|
+
'requests_per_day': 0,
|
332
|
+
'has_access': False
|
333
|
+
}
|
334
|
+
|
335
|
+
return {
|
336
|
+
'requests_per_hour': subscription.requests_per_hour,
|
337
|
+
'requests_per_day': subscription.requests_per_day,
|
338
|
+
'has_access': True,
|
339
|
+
'tier': subscription.tier,
|
340
|
+
'expires_at': subscription.expires_at
|
341
|
+
}
|
342
|
+
|
343
|
+
# Maintenance methods
|
344
|
+
def cleanup_expired(self, dry_run=True):
|
345
|
+
"""
|
346
|
+
Mark expired subscriptions as expired status.
|
347
|
+
|
348
|
+
Args:
|
349
|
+
dry_run: If True, only return count without making changes
|
350
|
+
|
351
|
+
Returns:
|
352
|
+
int: Number of subscriptions that would be/were updated
|
353
|
+
"""
|
354
|
+
expired_subscriptions = self.filter(
|
355
|
+
expires_at__lte=timezone.now(),
|
356
|
+
status__in=['active', 'suspended']
|
357
|
+
)
|
358
|
+
count = expired_subscriptions.count()
|
359
|
+
|
360
|
+
if not dry_run and count > 0:
|
361
|
+
expired_subscriptions.update(status='expired')
|
362
|
+
logger.info(f"Marked {count} subscriptions as expired")
|
363
|
+
|
364
|
+
return count
|
365
|
+
|
366
|
+
def process_auto_renewals(self, dry_run=True):
|
367
|
+
"""
|
368
|
+
Process auto-renewal for subscriptions expiring soon.
|
369
|
+
|
370
|
+
Args:
|
371
|
+
dry_run: If True, only return count without making changes
|
372
|
+
|
373
|
+
Returns:
|
374
|
+
int: Number of subscriptions that would be/were renewed
|
375
|
+
"""
|
376
|
+
# Get subscriptions expiring in the next 24 hours with auto-renewal
|
377
|
+
expiring_subscriptions = self.filter(
|
378
|
+
expires_at__lte=timezone.now() + timedelta(hours=24),
|
379
|
+
expires_at__gt=timezone.now(),
|
380
|
+
auto_renew=True,
|
381
|
+
status='active'
|
382
|
+
)
|
383
|
+
|
384
|
+
count = expiring_subscriptions.count()
|
385
|
+
|
386
|
+
if not dry_run and count > 0:
|
387
|
+
for subscription in expiring_subscriptions:
|
388
|
+
try:
|
389
|
+
subscription.renew(duration_days=30)
|
390
|
+
logger.info(f"Auto-renewed subscription", extra={
|
391
|
+
'subscription_id': str(subscription.id),
|
392
|
+
'user_id': subscription.user.id
|
393
|
+
})
|
394
|
+
except Exception as e:
|
395
|
+
logger.error(f"Failed to auto-renew subscription: {e}", extra={
|
396
|
+
'subscription_id': str(subscription.id),
|
397
|
+
'user_id': subscription.user.id
|
398
|
+
})
|
399
|
+
|
400
|
+
return count
|
401
|
+
|
402
|
+
# Statistics methods
|
403
|
+
def get_subscription_stats(self, days=30):
|
404
|
+
"""
|
405
|
+
Get subscription statistics for the last N days.
|
406
|
+
|
407
|
+
Args:
|
408
|
+
days: Number of days to analyze (default: 30)
|
409
|
+
|
410
|
+
Returns:
|
411
|
+
dict: Subscription statistics
|
412
|
+
"""
|
413
|
+
queryset = self.get_queryset()
|
414
|
+
recent_queryset = queryset.created_recently(days)
|
415
|
+
|
416
|
+
stats = {
|
417
|
+
'total_subscriptions': queryset.count(),
|
418
|
+
'active_subscriptions': queryset.active().count(),
|
419
|
+
'expired_subscriptions': queryset.expired().count(),
|
420
|
+
'new_subscriptions': recent_queryset.count(),
|
421
|
+
'total_revenue': queryset.total_revenue(),
|
422
|
+
'average_cost': queryset.average_cost(),
|
423
|
+
'by_tier': list(queryset.count_by_tier()),
|
424
|
+
'by_status': list(queryset.count_by_status()),
|
425
|
+
'usage_stats': queryset.usage_stats(),
|
426
|
+
'auto_renewing': queryset.auto_renewing().count(),
|
427
|
+
'expiring_soon': queryset.expiring_soon(7).count(),
|
428
|
+
}
|
429
|
+
|
430
|
+
logger.info(f"Generated subscription stats for {days} days", extra={
|
431
|
+
'days': days,
|
432
|
+
'total_subscriptions': stats['total_subscriptions'],
|
433
|
+
'active_subscriptions': stats['active_subscriptions']
|
434
|
+
})
|
435
|
+
|
436
|
+
return stats
|
437
|
+
|
438
|
+
def get_tier_analytics(self):
|
439
|
+
"""
|
440
|
+
Get detailed analytics by subscription tier.
|
441
|
+
|
442
|
+
Returns:
|
443
|
+
dict: Tier-based analytics
|
444
|
+
"""
|
445
|
+
analytics = {}
|
446
|
+
|
447
|
+
for tier_code, tier_name in self.model.SubscriptionTier.choices:
|
448
|
+
tier_subscriptions = self.by_tier(tier_code)
|
449
|
+
|
450
|
+
analytics[tier_code] = {
|
451
|
+
'name': tier_name,
|
452
|
+
'total_count': tier_subscriptions.count(),
|
453
|
+
'active_count': tier_subscriptions.active().count(),
|
454
|
+
'revenue': tier_subscriptions.total_revenue(),
|
455
|
+
'average_usage': tier_subscriptions.aggregate(
|
456
|
+
avg=models.Avg('total_requests')
|
457
|
+
)['avg'] or 0,
|
458
|
+
'conversion_rate': 0.0 # Would need additional logic for conversion tracking
|
459
|
+
}
|
460
|
+
|
461
|
+
return analytics
|
462
|
+
|
463
|
+
# Business logic methods
|
464
|
+
def activate_subscription(self, subscription_id):
|
465
|
+
"""
|
466
|
+
Activate subscription (business logic in manager).
|
467
|
+
|
468
|
+
Args:
|
469
|
+
subscription_id: Subscription ID or instance
|
470
|
+
|
471
|
+
Returns:
|
472
|
+
bool: True if subscription was activated successfully
|
473
|
+
"""
|
474
|
+
try:
|
475
|
+
if isinstance(subscription_id, str):
|
476
|
+
subscription = self.get(id=subscription_id)
|
477
|
+
else:
|
478
|
+
subscription = subscription_id
|
479
|
+
|
480
|
+
subscription.status = subscription.model.SubscriptionStatus.ACTIVE
|
481
|
+
subscription.save(update_fields=['status', 'updated_at'])
|
482
|
+
|
483
|
+
logger.info(f"Subscription activated", extra={
|
484
|
+
'subscription_id': str(subscription.id),
|
485
|
+
'user_id': subscription.user.id
|
486
|
+
})
|
487
|
+
|
488
|
+
return True
|
489
|
+
|
490
|
+
except Exception as e:
|
491
|
+
logger.error(f"Failed to activate subscription: {e}", extra={
|
492
|
+
'subscription_id': str(subscription_id) if hasattr(subscription_id, 'id') else subscription_id
|
493
|
+
})
|
494
|
+
return False
|
495
|
+
|
496
|
+
def suspend_subscription(self, subscription_id, reason=None):
|
497
|
+
"""
|
498
|
+
Suspend subscription (business logic in manager).
|
499
|
+
|
500
|
+
Args:
|
501
|
+
subscription_id: Subscription ID or instance
|
502
|
+
reason: Suspension reason
|
503
|
+
|
504
|
+
Returns:
|
505
|
+
bool: True if subscription was suspended successfully
|
506
|
+
"""
|
507
|
+
try:
|
508
|
+
if isinstance(subscription_id, str):
|
509
|
+
subscription = self.get(id=subscription_id)
|
510
|
+
else:
|
511
|
+
subscription = subscription_id
|
512
|
+
|
513
|
+
subscription.status = subscription.model.SubscriptionStatus.SUSPENDED
|
514
|
+
subscription.save(update_fields=['status', 'updated_at'])
|
515
|
+
|
516
|
+
logger.warning(f"Subscription suspended", extra={
|
517
|
+
'subscription_id': str(subscription.id),
|
518
|
+
'user_id': subscription.user.id,
|
519
|
+
'reason': reason
|
520
|
+
})
|
521
|
+
|
522
|
+
return True
|
523
|
+
|
524
|
+
except Exception as e:
|
525
|
+
logger.error(f"Failed to suspend subscription: {e}", extra={
|
526
|
+
'subscription_id': str(subscription_id) if hasattr(subscription_id, 'id') else subscription_id
|
527
|
+
})
|
528
|
+
return False
|
529
|
+
|
530
|
+
def cancel_subscription(self, subscription_id, reason=None):
|
531
|
+
"""
|
532
|
+
Cancel subscription (business logic in manager).
|
533
|
+
|
534
|
+
Args:
|
535
|
+
subscription_id: Subscription ID or instance
|
536
|
+
reason: Cancellation reason
|
537
|
+
|
538
|
+
Returns:
|
539
|
+
bool: True if subscription was cancelled successfully
|
540
|
+
"""
|
541
|
+
try:
|
542
|
+
if isinstance(subscription_id, str):
|
543
|
+
subscription = self.get(id=subscription_id)
|
544
|
+
else:
|
545
|
+
subscription = subscription_id
|
546
|
+
|
547
|
+
subscription.status = subscription.model.SubscriptionStatus.CANCELLED
|
548
|
+
subscription.save(update_fields=['status', 'updated_at'])
|
549
|
+
|
550
|
+
logger.info(f"Subscription cancelled", extra={
|
551
|
+
'subscription_id': str(subscription.id),
|
552
|
+
'user_id': subscription.user.id,
|
553
|
+
'reason': reason
|
554
|
+
})
|
555
|
+
|
556
|
+
return True
|
557
|
+
|
558
|
+
except Exception as e:
|
559
|
+
logger.error(f"Failed to cancel subscription: {e}", extra={
|
560
|
+
'subscription_id': str(subscription_id) if hasattr(subscription_id, 'id') else subscription_id
|
561
|
+
})
|
562
|
+
return False
|
563
|
+
|
564
|
+
def renew_subscription(self, subscription_id, duration_days=30):
|
565
|
+
"""
|
566
|
+
Renew subscription (business logic in manager).
|
567
|
+
|
568
|
+
Args:
|
569
|
+
subscription_id: Subscription ID or instance
|
570
|
+
duration_days: Duration in days to extend
|
571
|
+
|
572
|
+
Returns:
|
573
|
+
bool: True if subscription was renewed successfully
|
574
|
+
"""
|
575
|
+
try:
|
576
|
+
if isinstance(subscription_id, str):
|
577
|
+
subscription = self.get(id=subscription_id)
|
578
|
+
else:
|
579
|
+
subscription = subscription_id
|
580
|
+
|
581
|
+
from datetime import timedelta
|
582
|
+
|
583
|
+
if subscription.is_expired:
|
584
|
+
# If expired, start from now
|
585
|
+
subscription.starts_at = timezone.now()
|
586
|
+
subscription.expires_at = subscription.starts_at + timedelta(days=duration_days)
|
587
|
+
else:
|
588
|
+
# If not expired, extend from current expiration
|
589
|
+
subscription.expires_at += timedelta(days=duration_days)
|
590
|
+
|
591
|
+
subscription.status = subscription.model.SubscriptionStatus.ACTIVE
|
592
|
+
subscription.save(update_fields=['starts_at', 'expires_at', 'status', 'updated_at'])
|
593
|
+
|
594
|
+
logger.info(f"Subscription renewed", extra={
|
595
|
+
'subscription_id': str(subscription.id),
|
596
|
+
'user_id': subscription.user.id,
|
597
|
+
'duration_days': duration_days,
|
598
|
+
'new_expires_at': subscription.expires_at.isoformat()
|
599
|
+
})
|
600
|
+
|
601
|
+
return True
|
602
|
+
|
603
|
+
except Exception as e:
|
604
|
+
logger.error(f"Failed to renew subscription: {e}", extra={
|
605
|
+
'subscription_id': str(subscription_id) if hasattr(subscription_id, 'id') else subscription_id
|
606
|
+
})
|
607
|
+
return False
|
608
|
+
|
609
|
+
def increment_subscription_usage(self, subscription_id):
|
610
|
+
"""
|
611
|
+
Increment usage counter for subscription (business logic in manager).
|
612
|
+
|
613
|
+
Args:
|
614
|
+
subscription_id: Subscription ID or instance
|
615
|
+
|
616
|
+
Returns:
|
617
|
+
bool: True if usage was incremented successfully
|
618
|
+
"""
|
619
|
+
try:
|
620
|
+
if isinstance(subscription_id, str):
|
621
|
+
subscription = self.get(id=subscription_id)
|
622
|
+
else:
|
623
|
+
subscription = subscription_id
|
624
|
+
|
625
|
+
subscription.total_requests += 1
|
626
|
+
subscription.last_request_at = timezone.now()
|
627
|
+
subscription.save(update_fields=['total_requests', 'last_request_at', 'updated_at'])
|
628
|
+
|
629
|
+
logger.debug(f"Incremented subscription usage", extra={
|
630
|
+
'subscription_id': str(subscription.id),
|
631
|
+
'user_id': subscription.user.id,
|
632
|
+
'total_requests': subscription.total_requests
|
633
|
+
})
|
634
|
+
|
635
|
+
return True
|
636
|
+
|
637
|
+
except Exception as e:
|
638
|
+
logger.error(f"Failed to increment subscription usage: {e}", extra={
|
639
|
+
'subscription_id': str(subscription_id) if hasattr(subscription_id, 'id') else subscription_id
|
640
|
+
})
|
641
|
+
return False
|