django-cfg 1.2.31__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 -10
- 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 +526 -222
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +465 -70
- 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 +303 -151
- django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
- 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 +342 -152
- django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +13 -18
- 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 +172 -148
- 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 -285
- 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 +346 -467
- django_cfg/apps/payments/services/core/subscription_service.py +425 -481
- 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 +234 -174
- django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
- django_cfg/apps/payments/services/providers/registry.py +367 -301
- 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 +210 -129
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -103
- django_cfg/apps/payments/signals/subscription_signals.py +194 -142
- 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 +45 -48
- django_cfg/apps/payments/urls_admin.py +33 -42
- 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/config.py +1 -1
- django_cfg/core/config.py +40 -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 -492
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +64 -16
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
- 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 -146
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
- django_cfg/apps/payments/managers/__init__.py +0 -23
- 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 -306
- django_cfg/apps/payments/managers/payment_manager.py +0 -192
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -57
- 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 -63
- 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 -461
- 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 -76
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
- django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
- django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
- django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
- django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
- django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -635
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
- 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/admin/payments/currency/change_list.html +0 -50
- 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 -43
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
- django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
- django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
- django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
- django_cfg/apps/payments/templates/payments/stats.html +0 -261
- django_cfg/apps/payments/templates/payments/test.html +0 -213
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/utils/__init__.py +0 -43
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -239
- 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 -63
- 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 -122
- 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 -451
- django_cfg/apps/payments/views/templates/base.py +0 -212
- 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 -158
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -244
- 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 -66
- django_cfg/core/integration.py +0 -160
- 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.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,146 +1,165 @@
|
|
1
1
|
"""
|
2
|
-
Subscription models for the
|
2
|
+
Subscription models for the Universal Payment System v2.0.
|
3
|
+
|
4
|
+
Handles user subscriptions and API access control.
|
3
5
|
"""
|
4
6
|
|
5
7
|
from django.db import models
|
6
8
|
from django.contrib.auth import get_user_model
|
7
|
-
from django.core.validators import MinValueValidator
|
9
|
+
from django.core.validators import MinValueValidator, MaxValueValidator
|
10
|
+
from django.core.exceptions import ValidationError
|
8
11
|
from django.utils import timezone
|
9
12
|
from datetime import timedelta
|
10
|
-
from .base import UUIDTimestampedModel
|
13
|
+
from .base import UUIDTimestampedModel
|
11
14
|
|
12
15
|
User = get_user_model()
|
13
16
|
|
14
17
|
|
15
|
-
class EndpointGroup(
|
16
|
-
"""
|
18
|
+
class EndpointGroup(models.Model):
|
19
|
+
"""
|
20
|
+
API endpoint group for subscription management.
|
21
|
+
|
22
|
+
Groups related API endpoints for subscription-based access control.
|
23
|
+
"""
|
17
24
|
|
18
25
|
name = models.CharField(
|
19
26
|
max_length=100,
|
20
27
|
unique=True,
|
21
|
-
help_text="Endpoint group name"
|
28
|
+
help_text="Endpoint group name (e.g., 'Payment API', 'Balance API')"
|
22
29
|
)
|
23
|
-
|
24
|
-
|
25
|
-
|
30
|
+
|
31
|
+
code = models.CharField(
|
32
|
+
max_length=50,
|
33
|
+
unique=True,
|
34
|
+
help_text="Endpoint group code (e.g., 'payments', 'balance')"
|
26
35
|
)
|
36
|
+
|
27
37
|
description = models.TextField(
|
28
38
|
blank=True,
|
29
|
-
help_text="
|
39
|
+
help_text="Description of what this endpoint group provides"
|
30
40
|
)
|
31
41
|
|
32
|
-
#
|
33
|
-
|
34
|
-
default=
|
35
|
-
|
36
|
-
help_text="Basic tier monthly price"
|
37
|
-
)
|
38
|
-
premium_price = models.FloatField(
|
39
|
-
default=0.0,
|
40
|
-
validators=[MinValueValidator(0.0)],
|
41
|
-
help_text="Premium tier monthly price"
|
42
|
-
)
|
43
|
-
enterprise_price = models.FloatField(
|
44
|
-
default=0.0,
|
45
|
-
validators=[MinValueValidator(0.0)],
|
46
|
-
help_text="Enterprise tier monthly price"
|
47
|
-
)
|
48
|
-
|
49
|
-
# Usage limits per tier
|
50
|
-
basic_limit = models.PositiveIntegerField(
|
51
|
-
default=1000,
|
52
|
-
help_text="Basic tier monthly usage limit"
|
53
|
-
)
|
54
|
-
premium_limit = models.PositiveIntegerField(
|
55
|
-
default=10000,
|
56
|
-
help_text="Premium tier monthly usage limit"
|
57
|
-
)
|
58
|
-
enterprise_limit = models.PositiveIntegerField(
|
59
|
-
default=0, # 0 = unlimited
|
60
|
-
help_text="Enterprise tier monthly usage limit (0 = unlimited)"
|
42
|
+
# Access control
|
43
|
+
is_enabled = models.BooleanField(
|
44
|
+
default=True,
|
45
|
+
help_text="Whether this endpoint group is available"
|
61
46
|
)
|
62
47
|
|
63
|
-
|
64
|
-
is_active = models.BooleanField(
|
48
|
+
requires_subscription = models.BooleanField(
|
65
49
|
default=True,
|
66
|
-
help_text="
|
50
|
+
help_text="Whether access requires an active subscription"
|
67
51
|
)
|
68
|
-
|
69
|
-
|
70
|
-
|
52
|
+
|
53
|
+
# Rate limiting defaults
|
54
|
+
default_rate_limit = models.PositiveIntegerField(
|
55
|
+
default=1000,
|
56
|
+
help_text="Default requests per hour for this endpoint group"
|
71
57
|
)
|
72
58
|
|
73
|
-
#
|
74
|
-
|
75
|
-
|
59
|
+
# Timestamps
|
60
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
61
|
+
updated_at = models.DateTimeField(auto_now=True)
|
76
62
|
|
77
63
|
class Meta:
|
78
|
-
db_table = '
|
79
|
-
verbose_name =
|
80
|
-
verbose_name_plural =
|
81
|
-
indexes = [
|
82
|
-
models.Index(fields=['name']),
|
83
|
-
models.Index(fields=['is_active']),
|
84
|
-
]
|
64
|
+
db_table = 'payments_endpoint_groups'
|
65
|
+
verbose_name = 'Endpoint Group'
|
66
|
+
verbose_name_plural = 'Endpoint Groups'
|
85
67
|
ordering = ['name']
|
86
68
|
|
87
69
|
def __str__(self):
|
88
|
-
return self.
|
89
|
-
|
90
|
-
def
|
91
|
-
"""
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
def
|
100
|
-
"""
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
70
|
+
return f"{self.name} ({self.code})"
|
71
|
+
|
72
|
+
def clean(self):
|
73
|
+
"""Validate endpoint group data."""
|
74
|
+
if self.code:
|
75
|
+
self.code = self.code.lower().replace(' ', '_')
|
76
|
+
|
77
|
+
|
78
|
+
class SubscriptionQuerySet(models.QuerySet):
|
79
|
+
"""Optimized queryset for subscription operations."""
|
80
|
+
|
81
|
+
def optimized(self):
|
82
|
+
"""Prevent N+1 queries."""
|
83
|
+
return self.select_related('user').prefetch_related('endpoint_groups')
|
84
|
+
|
85
|
+
def active(self):
|
86
|
+
"""Get active subscriptions."""
|
87
|
+
return self.filter(
|
88
|
+
status=Subscription.SubscriptionStatus.ACTIVE,
|
89
|
+
expires_at__gt=timezone.now()
|
90
|
+
)
|
91
|
+
|
92
|
+
def expired(self):
|
93
|
+
"""Get expired subscriptions."""
|
94
|
+
return self.filter(expires_at__lte=timezone.now())
|
95
|
+
|
96
|
+
def by_tier(self, tier):
|
97
|
+
"""Filter by subscription tier."""
|
98
|
+
return self.filter(tier=tier)
|
99
|
+
|
100
|
+
def by_user(self, user):
|
101
|
+
"""Filter by user."""
|
102
|
+
return self.filter(user=user)
|
103
|
+
|
104
|
+
|
105
|
+
class SubscriptionManager(models.Manager):
|
106
|
+
"""Manager for subscription operations."""
|
107
|
+
|
108
|
+
def get_queryset(self):
|
109
|
+
"""Return optimized queryset by default."""
|
110
|
+
return SubscriptionQuerySet(self.model, using=self._db)
|
111
|
+
|
112
|
+
def optimized(self):
|
113
|
+
"""Get optimized queryset."""
|
114
|
+
return self.get_queryset().optimized()
|
115
|
+
|
116
|
+
def active(self):
|
117
|
+
"""Get active subscriptions."""
|
118
|
+
return self.get_queryset().active()
|
119
|
+
|
120
|
+
def expired(self):
|
121
|
+
"""Get expired subscriptions."""
|
122
|
+
return self.get_queryset().expired()
|
123
|
+
|
124
|
+
def by_tier(self, tier):
|
125
|
+
"""Get subscriptions by tier."""
|
126
|
+
return self.get_queryset().by_tier(tier)
|
107
127
|
|
108
128
|
|
109
129
|
class Subscription(UUIDTimestampedModel):
|
110
|
-
"""
|
130
|
+
"""
|
131
|
+
User subscription model for API access control.
|
132
|
+
|
133
|
+
Manages user subscriptions with different tiers and access levels.
|
134
|
+
"""
|
111
135
|
|
112
136
|
class SubscriptionStatus(models.TextChoices):
|
113
137
|
ACTIVE = "active", "Active"
|
114
138
|
INACTIVE = "inactive", "Inactive"
|
115
|
-
EXPIRED = "expired", "Expired"
|
116
|
-
CANCELLED = "cancelled", "Cancelled"
|
117
139
|
SUSPENDED = "suspended", "Suspended"
|
140
|
+
CANCELLED = "cancelled", "Cancelled"
|
141
|
+
EXPIRED = "expired", "Expired"
|
118
142
|
|
119
143
|
class SubscriptionTier(models.TextChoices):
|
120
|
-
|
121
|
-
|
122
|
-
|
144
|
+
FREE = "free", "Free Tier"
|
145
|
+
BASIC = "basic", "Basic Tier"
|
146
|
+
PRO = "pro", "Pro Tier"
|
147
|
+
ENTERPRISE = "enterprise", "Enterprise Tier"
|
123
148
|
|
124
149
|
user = models.ForeignKey(
|
125
150
|
User,
|
126
151
|
on_delete=models.CASCADE,
|
127
|
-
related_name='
|
128
|
-
help_text="
|
129
|
-
)
|
130
|
-
endpoint_group = models.ForeignKey(
|
131
|
-
EndpointGroup,
|
132
|
-
on_delete=models.CASCADE,
|
133
|
-
related_name='subscriptions',
|
134
|
-
help_text="Endpoint group"
|
152
|
+
related_name='payment_subscriptions',
|
153
|
+
help_text="User who owns this subscription"
|
135
154
|
)
|
136
155
|
|
137
|
-
# Subscription details
|
138
156
|
tier = models.CharField(
|
139
157
|
max_length=20,
|
140
158
|
choices=SubscriptionTier.choices,
|
141
|
-
default=SubscriptionTier.
|
159
|
+
default=SubscriptionTier.FREE,
|
142
160
|
help_text="Subscription tier"
|
143
161
|
)
|
162
|
+
|
144
163
|
status = models.CharField(
|
145
164
|
max_length=20,
|
146
165
|
choices=SubscriptionStatus.choices,
|
@@ -148,123 +167,184 @@ class Subscription(UUIDTimestampedModel):
|
|
148
167
|
help_text="Subscription status"
|
149
168
|
)
|
150
169
|
|
151
|
-
#
|
152
|
-
|
153
|
-
|
154
|
-
|
170
|
+
# Access control
|
171
|
+
endpoint_groups = models.ManyToManyField(
|
172
|
+
EndpointGroup,
|
173
|
+
related_name='subscriptions',
|
174
|
+
blank=True,
|
175
|
+
help_text="Endpoint groups accessible with this subscription"
|
155
176
|
)
|
156
177
|
|
157
|
-
#
|
158
|
-
|
178
|
+
# Rate limiting
|
179
|
+
requests_per_hour = models.PositiveIntegerField(
|
180
|
+
default=100,
|
181
|
+
validators=[MinValueValidator(1), MaxValueValidator(100000)],
|
182
|
+
help_text="API requests allowed per hour"
|
183
|
+
)
|
184
|
+
|
185
|
+
requests_per_day = models.PositiveIntegerField(
|
159
186
|
default=1000,
|
160
|
-
|
187
|
+
validators=[MinValueValidator(1), MaxValueValidator(1000000)],
|
188
|
+
help_text="API requests allowed per day"
|
161
189
|
)
|
162
|
-
|
163
|
-
|
164
|
-
|
190
|
+
|
191
|
+
# Subscription period
|
192
|
+
starts_at = models.DateTimeField(
|
193
|
+
default=timezone.now,
|
194
|
+
help_text="When this subscription starts"
|
165
195
|
)
|
166
196
|
|
167
|
-
|
168
|
-
|
169
|
-
null=True,
|
170
|
-
blank=True,
|
171
|
-
help_text="Last billing date"
|
197
|
+
expires_at = models.DateTimeField(
|
198
|
+
help_text="When this subscription expires"
|
172
199
|
)
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
200
|
+
|
201
|
+
# Billing information
|
202
|
+
monthly_cost_usd = models.FloatField(
|
203
|
+
default=0.0,
|
204
|
+
validators=[MinValueValidator(0.0)],
|
205
|
+
help_text="Monthly cost in USD"
|
177
206
|
)
|
178
207
|
|
179
|
-
#
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
help_text="Subscription expiration"
|
208
|
+
# Usage tracking
|
209
|
+
total_requests = models.PositiveIntegerField(
|
210
|
+
default=0,
|
211
|
+
help_text="Total API requests made with this subscription"
|
184
212
|
)
|
185
|
-
|
213
|
+
|
214
|
+
last_request_at = models.DateTimeField(
|
186
215
|
null=True,
|
187
216
|
blank=True,
|
188
|
-
help_text="
|
217
|
+
help_text="When the last API request was made"
|
189
218
|
)
|
190
219
|
|
191
|
-
#
|
192
|
-
|
193
|
-
default=
|
194
|
-
help_text="
|
220
|
+
# Auto-renewal
|
221
|
+
auto_renew = models.BooleanField(
|
222
|
+
default=False,
|
223
|
+
help_text="Whether to automatically renew this subscription"
|
195
224
|
)
|
196
225
|
|
197
|
-
#
|
198
|
-
from
|
226
|
+
# Manager
|
227
|
+
from .managers.subscription_managers import SubscriptionManager
|
199
228
|
objects = SubscriptionManager()
|
200
229
|
|
201
230
|
class Meta:
|
202
|
-
db_table = '
|
203
|
-
verbose_name =
|
204
|
-
verbose_name_plural =
|
231
|
+
db_table = 'payments_subscriptions'
|
232
|
+
verbose_name = 'Subscription'
|
233
|
+
verbose_name_plural = 'Subscriptions'
|
234
|
+
ordering = ['-created_at']
|
205
235
|
indexes = [
|
206
236
|
models.Index(fields=['user', 'status']),
|
207
|
-
models.Index(fields=['endpoint_group', 'status']),
|
208
237
|
models.Index(fields=['status', 'expires_at']),
|
209
|
-
models.Index(fields=['
|
210
|
-
|
238
|
+
models.Index(fields=['tier', 'status']),
|
239
|
+
]
|
240
|
+
constraints = [
|
241
|
+
models.UniqueConstraint(
|
242
|
+
fields=['user'],
|
243
|
+
condition=models.Q(status='active'),
|
244
|
+
name='one_active_subscription_per_user'
|
245
|
+
),
|
211
246
|
]
|
212
|
-
unique_together = [['user', 'endpoint_group']] # One subscription per user per group
|
213
|
-
ordering = ['-created_at']
|
214
247
|
|
215
248
|
def __str__(self):
|
216
|
-
return f"{self.user.
|
249
|
+
return f"{self.user.username} - {self.tier} ({self.status})"
|
217
250
|
|
218
|
-
def
|
219
|
-
"""
|
220
|
-
|
251
|
+
def save(self, *args, **kwargs):
|
252
|
+
"""Override save to set default expiration."""
|
253
|
+
if not self.expires_at:
|
254
|
+
# Default to 30 days from start
|
255
|
+
self.expires_at = self.starts_at + timedelta(days=30)
|
221
256
|
|
257
|
+
super().save(*args, **kwargs)
|
258
|
+
|
259
|
+
def clean(self):
|
260
|
+
"""Validate subscription data."""
|
261
|
+
if self.expires_at and self.starts_at and self.expires_at <= self.starts_at:
|
262
|
+
raise ValidationError("Expiration date must be after start date")
|
263
|
+
|
264
|
+
if self.requests_per_day < self.requests_per_hour:
|
265
|
+
raise ValidationError("Daily limit cannot be less than hourly limit")
|
266
|
+
|
267
|
+
@property
|
268
|
+
def is_active(self) -> bool:
|
269
|
+
"""Check if subscription is active and not expired."""
|
222
270
|
return (
|
223
271
|
self.status == self.SubscriptionStatus.ACTIVE and
|
224
|
-
|
272
|
+
self.expires_at > timezone.now()
|
225
273
|
)
|
226
274
|
|
227
|
-
|
228
|
-
|
229
|
-
|
275
|
+
@property
|
276
|
+
def is_expired(self) -> bool:
|
277
|
+
"""Check if subscription is expired."""
|
278
|
+
return timezone.now() > self.expires_at
|
230
279
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
def
|
248
|
-
"""
|
249
|
-
self.
|
250
|
-
|
251
|
-
|
252
|
-
def
|
253
|
-
"""
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
280
|
+
@property
|
281
|
+
def days_remaining(self) -> int:
|
282
|
+
"""Get days remaining until expiration."""
|
283
|
+
if self.is_expired:
|
284
|
+
return 0
|
285
|
+
delta = self.expires_at - timezone.now()
|
286
|
+
return max(0, delta.days)
|
287
|
+
|
288
|
+
@property
|
289
|
+
def usage_percentage(self) -> float:
|
290
|
+
"""Get usage percentage for current period."""
|
291
|
+
# This would need to be calculated based on actual usage tracking
|
292
|
+
# For now, return 0.0 as placeholder
|
293
|
+
return 0.0
|
294
|
+
|
295
|
+
@property
|
296
|
+
def tier_display(self) -> str:
|
297
|
+
"""Get display name for tier."""
|
298
|
+
return self.get_tier_display()
|
299
|
+
|
300
|
+
@property
|
301
|
+
def status_color(self) -> str:
|
302
|
+
"""Get color for status display."""
|
303
|
+
colors = {
|
304
|
+
self.SubscriptionStatus.ACTIVE: 'success',
|
305
|
+
self.SubscriptionStatus.INACTIVE: 'secondary',
|
306
|
+
self.SubscriptionStatus.SUSPENDED: 'warning',
|
307
|
+
self.SubscriptionStatus.CANCELLED: 'danger',
|
308
|
+
self.SubscriptionStatus.EXPIRED: 'danger',
|
309
|
+
}
|
310
|
+
return colors.get(self.status, 'secondary')
|
311
|
+
|
312
|
+
def activate(self):
|
313
|
+
"""Activate subscription (delegates to manager)."""
|
314
|
+
return self.__class__.objects.activate_subscription(self)
|
315
|
+
|
316
|
+
def suspend(self, reason=None):
|
317
|
+
"""Suspend subscription (delegates to manager)."""
|
318
|
+
return self.__class__.objects.suspend_subscription(self, reason)
|
319
|
+
|
320
|
+
def cancel(self, reason=None):
|
321
|
+
"""Cancel subscription (delegates to manager)."""
|
322
|
+
return self.__class__.objects.cancel_subscription(self, reason)
|
323
|
+
|
324
|
+
def renew(self, duration_days: int = 30):
|
325
|
+
"""Renew subscription (delegates to manager)."""
|
326
|
+
return self.__class__.objects.renew_subscription(self, duration_days)
|
327
|
+
|
328
|
+
def has_access_to_endpoint_group(self, endpoint_group_code: str) -> bool:
|
329
|
+
"""Check if subscription has access to specific endpoint group."""
|
330
|
+
if not self.is_active:
|
331
|
+
return False
|
269
332
|
|
270
|
-
self.
|
333
|
+
return self.endpoint_groups.filter(
|
334
|
+
code=endpoint_group_code,
|
335
|
+
is_enabled=True
|
336
|
+
).exists()
|
337
|
+
|
338
|
+
def increment_usage(self):
|
339
|
+
"""Increment usage counter (delegates to manager)."""
|
340
|
+
return self.__class__.objects.increment_subscription_usage(self)
|
341
|
+
|
342
|
+
@classmethod
|
343
|
+
def get_active_for_user(cls, user: User) -> 'Subscription':
|
344
|
+
"""Get active subscription for user (delegates to manager)."""
|
345
|
+
return cls.objects.get_active_for_user(user)
|
346
|
+
|
347
|
+
@classmethod
|
348
|
+
def create_free_subscription(cls, user: User) -> 'Subscription':
|
349
|
+
"""Create free tier subscription for user (delegates to manager)."""
|
350
|
+
return cls.objects.create_free_subscription(user)
|