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,611 +1,555 @@
|
|
1
1
|
"""
|
2
|
-
Subscription
|
2
|
+
Subscription service for the Universal Payment System v2.0.
|
3
3
|
|
4
|
-
|
5
|
-
and usage tracking with Redis caching.
|
4
|
+
Handles subscription management and access control.
|
6
5
|
"""
|
7
6
|
|
8
|
-
from typing import Dict, Any,
|
9
|
-
from
|
10
|
-
from
|
11
|
-
|
12
|
-
from django.db import transaction
|
13
|
-
from django.contrib.auth import get_user_model
|
7
|
+
from typing import Optional, Dict, Any, List
|
8
|
+
from django.contrib.auth.models import User
|
9
|
+
from django.db import models
|
14
10
|
from django.utils import timezone
|
15
|
-
from
|
16
|
-
from decimal import Decimal
|
11
|
+
from datetime import timedelta
|
17
12
|
|
13
|
+
from .base import BaseService
|
14
|
+
from ..types import (
|
15
|
+
SubscriptionCreateRequest, SubscriptionResult, SubscriptionData,
|
16
|
+
ServiceOperationResult
|
17
|
+
)
|
18
18
|
from ...models import Subscription, EndpointGroup, Tariff
|
19
|
-
from ..internal_types import ServiceOperationResult, SubscriptionInfo, EndpointGroupInfo
|
20
|
-
|
21
|
-
User = get_user_model()
|
22
|
-
logger = get_logger("subscription_service")
|
23
|
-
|
24
|
-
|
25
|
-
class SubscriptionRequest(BaseModel):
|
26
|
-
"""Type-safe subscription request"""
|
27
|
-
user_id: int = Field(gt=0, description="User ID")
|
28
|
-
endpoint_group_name: str = Field(min_length=1, description="Endpoint group name")
|
29
|
-
tariff_id: Optional[str] = Field(None, description="Specific tariff ID")
|
30
|
-
billing_period: str = Field(default='monthly', pattern='^(monthly|yearly)$', description="Billing period")
|
31
|
-
auto_renew: bool = Field(default=True, description="Auto-renewal setting")
|
32
|
-
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
33
|
-
|
34
|
-
|
35
|
-
class SubscriptionResult(BaseModel):
|
36
|
-
"""Type-safe subscription operation result"""
|
37
|
-
success: bool
|
38
|
-
subscription_id: Optional[str] = None
|
39
|
-
endpoint_group_id: Optional[str] = None
|
40
|
-
expires_at: Optional[datetime] = None
|
41
|
-
error_message: Optional[str] = None
|
42
|
-
error_code: Optional[str] = None
|
43
19
|
|
44
20
|
|
45
|
-
class
|
46
|
-
"""Type-safe access check result"""
|
47
|
-
allowed: bool
|
48
|
-
subscription_id: Optional[str] = None
|
49
|
-
reason: Optional[str] = None
|
50
|
-
remaining_requests: Optional[int] = None
|
51
|
-
usage_percentage: Optional[float] = None
|
52
|
-
required_subscription: Optional[str] = None
|
53
|
-
current_usage: Optional[int] = None
|
54
|
-
monthly_limit: Optional[int] = None
|
55
|
-
|
56
|
-
|
57
|
-
class SubscriptionService:
|
21
|
+
class SubscriptionService(BaseService):
|
58
22
|
"""
|
59
|
-
|
23
|
+
Subscription service with business logic and validation.
|
60
24
|
|
61
|
-
Handles subscription
|
62
|
-
with support for multiple active subscriptions per user.
|
25
|
+
Handles subscription operations using Pydantic validation and Django ORM managers.
|
63
26
|
"""
|
64
27
|
|
65
|
-
def
|
66
|
-
"""Initialize subscription service with dependencies"""
|
67
|
-
pass
|
68
|
-
|
69
|
-
def create_subscription(self, subscription_data: dict) -> 'ServiceOperationResult':
|
28
|
+
def create_subscription(self, request: SubscriptionCreateRequest) -> SubscriptionResult:
|
70
29
|
"""
|
71
30
|
Create new subscription for user.
|
72
31
|
|
73
32
|
Args:
|
74
|
-
|
33
|
+
request: Subscription creation request with validation
|
75
34
|
|
76
35
|
Returns:
|
77
|
-
|
36
|
+
SubscriptionResult: Created subscription information
|
78
37
|
"""
|
79
38
|
try:
|
39
|
+
# Validate request
|
40
|
+
if isinstance(request, dict):
|
41
|
+
request = SubscriptionCreateRequest(**request)
|
42
|
+
|
43
|
+
self.logger.info("Creating subscription", extra={
|
44
|
+
'user_id': request.user_id,
|
45
|
+
'tier': request.tier,
|
46
|
+
'duration_days': request.duration_days
|
47
|
+
})
|
48
|
+
|
80
49
|
# Get user
|
81
|
-
|
50
|
+
try:
|
51
|
+
user = User.objects.get(id=request.user_id)
|
52
|
+
except User.DoesNotExist:
|
53
|
+
return SubscriptionResult(
|
54
|
+
success=False,
|
55
|
+
message=f"User {request.user_id} not found",
|
56
|
+
error_code="user_not_found"
|
57
|
+
)
|
58
|
+
|
59
|
+
# Get tariff for tier
|
60
|
+
try:
|
61
|
+
tariff = Tariff.objects.get(tier=request.tier, is_active=True)
|
62
|
+
except Tariff.DoesNotExist:
|
63
|
+
return SubscriptionResult(
|
64
|
+
success=False,
|
65
|
+
message=f"Tariff for tier {request.tier} not found",
|
66
|
+
error_code="tariff_not_found"
|
67
|
+
)
|
82
68
|
|
83
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
|
69
|
+
# Cancel existing active subscriptions
|
70
|
+
existing_active = Subscription.objects.filter(
|
71
|
+
user=user,
|
72
|
+
status=Subscription.SubscriptionStatus.ACTIVE
|
87
73
|
)
|
88
74
|
|
89
|
-
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
endpoint_group=endpoint_group,
|
94
|
-
status=Subscription.SubscriptionStatus.ACTIVE,
|
95
|
-
expires_at__gt=timezone.now()
|
96
|
-
).first()
|
75
|
+
def create_subscription_transaction():
|
76
|
+
# Cancel existing subscriptions
|
77
|
+
for sub in existing_active:
|
78
|
+
sub.cancel("Replaced by new subscription")
|
97
79
|
|
98
|
-
|
99
|
-
|
100
|
-
success=False,
|
101
|
-
error_message=f"User already has active subscription for '{subscription_data['endpoint_group_name']}'"
|
102
|
-
)
|
80
|
+
# Create new subscription
|
81
|
+
expires_at = timezone.now() + timedelta(days=request.duration_days)
|
103
82
|
|
104
|
-
# Create subscription
|
105
83
|
subscription = Subscription.objects.create(
|
106
84
|
user=user,
|
107
|
-
|
108
|
-
tier=Subscription.SubscriptionTier.BASIC,
|
85
|
+
tier=request.tier,
|
109
86
|
status=Subscription.SubscriptionStatus.ACTIVE,
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
87
|
+
requests_per_hour=tariff.requests_per_hour,
|
88
|
+
requests_per_day=tariff.requests_per_day,
|
89
|
+
monthly_cost_usd=tariff.monthly_price_usd,
|
90
|
+
auto_renew=request.auto_renew,
|
91
|
+
expires_at=expires_at
|
115
92
|
)
|
116
93
|
|
117
|
-
#
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
94
|
+
# Add endpoint groups
|
95
|
+
if request.endpoint_groups:
|
96
|
+
endpoint_groups = EndpointGroup.objects.filter(
|
97
|
+
code__in=request.endpoint_groups,
|
98
|
+
is_enabled=True
|
99
|
+
)
|
100
|
+
subscription.endpoint_groups.set(endpoint_groups)
|
101
|
+
else:
|
102
|
+
# Add default endpoint groups for tier
|
103
|
+
default_groups = self._get_default_endpoint_groups(request.tier)
|
104
|
+
subscription.endpoint_groups.set(default_groups)
|
128
105
|
|
129
|
-
|
130
|
-
|
106
|
+
return subscription
|
107
|
+
|
108
|
+
subscription = self._execute_with_transaction(create_subscription_transaction)
|
109
|
+
|
110
|
+
# Convert to response data
|
111
|
+
subscription_data = SubscriptionData.model_validate(subscription)
|
112
|
+
|
113
|
+
self._log_operation(
|
114
|
+
"create_subscription",
|
115
|
+
True,
|
116
|
+
subscription_id=str(subscription.id),
|
117
|
+
user_id=request.user_id,
|
118
|
+
tier=request.tier
|
119
|
+
)
|
131
120
|
|
132
|
-
return
|
133
|
-
success=
|
134
|
-
|
121
|
+
return SubscriptionResult(
|
122
|
+
success=True,
|
123
|
+
message="Subscription created successfully",
|
124
|
+
subscription_id=str(subscription.id),
|
125
|
+
user_id=request.user_id,
|
126
|
+
tier=subscription.tier,
|
127
|
+
status=subscription.status,
|
128
|
+
expires_at=subscription.expires_at,
|
129
|
+
data={'subscription': subscription_data.model_dump()}
|
135
130
|
)
|
131
|
+
|
132
|
+
except Exception as e:
|
133
|
+
return SubscriptionResult(**self._handle_exception(
|
134
|
+
"create_subscription", e,
|
135
|
+
user_id=request.user_id if hasattr(request, 'user_id') else None
|
136
|
+
).model_dump())
|
136
137
|
|
137
|
-
def
|
138
|
-
self,
|
139
|
-
user: User,
|
140
|
-
endpoint_group_name: str,
|
141
|
-
use_cache: bool = True
|
142
|
-
) -> AccessCheck:
|
138
|
+
def get_user_subscription(self, user_id: int) -> SubscriptionResult:
|
143
139
|
"""
|
144
|
-
|
140
|
+
Get active subscription for user.
|
145
141
|
|
146
142
|
Args:
|
147
|
-
|
148
|
-
endpoint_group_name: Name of endpoint group
|
149
|
-
use_cache: Whether to use Redis cache
|
143
|
+
user_id: User ID
|
150
144
|
|
151
145
|
Returns:
|
152
|
-
|
146
|
+
SubscriptionResult: Active subscription or free tier
|
153
147
|
"""
|
154
148
|
try:
|
155
|
-
|
156
|
-
if use_cache:
|
157
|
-
cache_key = f"access:{user.id}:{endpoint_group_name}"
|
158
|
-
cached = self.cache.get_cache(cache_key)
|
159
|
-
if cached:
|
160
|
-
return AccessCheck(**cached)
|
161
|
-
|
162
|
-
# Check active subscription
|
163
|
-
subscription = Subscription.objects.filter(
|
164
|
-
user=user,
|
165
|
-
endpoint_group__name=endpoint_group_name,
|
166
|
-
status=Subscription.Status.ACTIVE,
|
167
|
-
expires_at__gt=timezone.now()
|
168
|
-
).select_related('endpoint_group', 'tariff').first()
|
149
|
+
self.logger.debug("Getting user subscription", extra={'user_id': user_id})
|
169
150
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
allowed=False,
|
179
|
-
reason='usage_limit_exceeded',
|
180
|
-
subscription_id=str(subscription.id),
|
181
|
-
current_usage=subscription.current_usage,
|
182
|
-
monthly_limit=subscription.get_monthly_limit()
|
183
|
-
)
|
184
|
-
else:
|
185
|
-
result = AccessCheck(
|
186
|
-
allowed=True,
|
187
|
-
subscription_id=str(subscription.id),
|
188
|
-
remaining_requests=subscription.remaining_requests(),
|
189
|
-
usage_percentage=subscription.usage_percentage
|
151
|
+
# Check user exists
|
152
|
+
try:
|
153
|
+
user = User.objects.get(id=user_id)
|
154
|
+
except User.DoesNotExist:
|
155
|
+
return SubscriptionResult(
|
156
|
+
success=False,
|
157
|
+
message=f"User {user_id} not found",
|
158
|
+
error_code="user_not_found"
|
190
159
|
)
|
191
160
|
|
192
|
-
#
|
193
|
-
|
194
|
-
cache_data = result.dict()
|
195
|
-
self.cache.set_cache(f"access:{user.id}:{endpoint_group_name}", cache_data, ttl=60)
|
161
|
+
# Get active subscription
|
162
|
+
subscription = Subscription.objects.get_active_for_user(user)
|
196
163
|
|
197
|
-
|
164
|
+
if not subscription:
|
165
|
+
# Create free subscription if none exists
|
166
|
+
subscription = Subscription.objects.create_free_subscription(user)
|
198
167
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
168
|
+
# Convert to response data
|
169
|
+
subscription_data = SubscriptionData.model_validate(subscription)
|
170
|
+
|
171
|
+
# Calculate requests remaining
|
172
|
+
requests_remaining = self._calculate_requests_remaining(subscription)
|
173
|
+
|
174
|
+
return SubscriptionResult(
|
175
|
+
success=True,
|
176
|
+
message="Subscription retrieved successfully",
|
177
|
+
subscription_id=str(subscription.id),
|
178
|
+
user_id=user_id,
|
179
|
+
tier=subscription.tier,
|
180
|
+
status=subscription.status,
|
181
|
+
expires_at=subscription.expires_at,
|
182
|
+
requests_remaining=requests_remaining,
|
183
|
+
data={'subscription': subscription_data.model_dump()}
|
204
184
|
)
|
205
|
-
|
206
|
-
def record_api_usage(
|
207
|
-
self,
|
208
|
-
user: User,
|
209
|
-
endpoint_group_name: str,
|
210
|
-
usage_count: int = 1
|
211
|
-
) -> bool:
|
212
|
-
"""
|
213
|
-
Record API usage for user's subscription.
|
214
|
-
|
215
|
-
Args:
|
216
|
-
user: User object
|
217
|
-
endpoint_group_name: Name of endpoint group
|
218
|
-
usage_count: Number of requests to record
|
219
185
|
|
220
|
-
Returns:
|
221
|
-
True if usage was recorded, False otherwise
|
222
|
-
"""
|
223
|
-
try:
|
224
|
-
with transaction.atomic():
|
225
|
-
subscription = Subscription.objects.filter(
|
226
|
-
user=user,
|
227
|
-
endpoint_group__name=endpoint_group_name,
|
228
|
-
status=Subscription.Status.ACTIVE,
|
229
|
-
expires_at__gt=timezone.now()
|
230
|
-
).first()
|
231
|
-
|
232
|
-
if not subscription:
|
233
|
-
logger.warning(f"No active subscription found for user {user.id}, endpoint {endpoint_group_name}")
|
234
|
-
return False
|
235
|
-
|
236
|
-
# Update usage
|
237
|
-
subscription.current_usage += usage_count
|
238
|
-
subscription.save(update_fields=['current_usage', 'updated_at'])
|
239
|
-
|
240
|
-
# Invalidate access cache
|
241
|
-
self.cache.delete_key(f"access:{user.id}:{endpoint_group_name}")
|
242
|
-
|
243
|
-
return True
|
244
|
-
|
245
186
|
except Exception as e:
|
246
|
-
|
247
|
-
|
187
|
+
return SubscriptionResult(**self._handle_exception(
|
188
|
+
"get_user_subscription", e,
|
189
|
+
user_id=user_id
|
190
|
+
).model_dump())
|
248
191
|
|
249
|
-
def
|
250
|
-
self,
|
251
|
-
user_id: int,
|
252
|
-
active_only: bool = True
|
253
|
-
) -> List['SubscriptionInfo']:
|
192
|
+
def check_access(self, user_id: int, endpoint_group: str) -> ServiceOperationResult:
|
254
193
|
"""
|
255
|
-
|
194
|
+
Check if user has access to endpoint group.
|
256
195
|
|
257
196
|
Args:
|
258
197
|
user_id: User ID
|
259
|
-
|
198
|
+
endpoint_group: Endpoint group code
|
260
199
|
|
261
200
|
Returns:
|
262
|
-
|
201
|
+
ServiceOperationResult: Access check result
|
263
202
|
"""
|
264
203
|
try:
|
204
|
+
self.logger.debug("Checking endpoint access", extra={
|
205
|
+
'user_id': user_id,
|
206
|
+
'endpoint_group': endpoint_group
|
207
|
+
})
|
208
|
+
|
209
|
+
# Get user subscription
|
210
|
+
subscription_result = self.get_user_subscription(user_id)
|
211
|
+
if not subscription_result.success:
|
212
|
+
return self._create_error_result(
|
213
|
+
subscription_result.message,
|
214
|
+
subscription_result.error_code
|
215
|
+
)
|
265
216
|
|
266
|
-
|
267
|
-
queryset = Subscription.objects.filter(user_id=user_id)
|
217
|
+
subscription = Subscription.objects.get(id=subscription_result.subscription_id)
|
268
218
|
|
269
|
-
if
|
270
|
-
|
271
|
-
|
272
|
-
|
219
|
+
# Check if subscription is active and not expired
|
220
|
+
if not subscription.is_active():
|
221
|
+
return self._create_error_result(
|
222
|
+
"Subscription is not active",
|
223
|
+
"subscription_inactive"
|
273
224
|
)
|
274
225
|
|
275
|
-
|
276
|
-
|
277
|
-
).order_by('-created_at')
|
278
|
-
|
279
|
-
result = [
|
280
|
-
SubscriptionInfo(
|
281
|
-
id=str(sub.id),
|
282
|
-
endpoint_group=EndpointGroupInfo(
|
283
|
-
id=str(sub.endpoint_group.id),
|
284
|
-
name=sub.endpoint_group.name,
|
285
|
-
display_name=sub.endpoint_group.display_name
|
286
|
-
),
|
287
|
-
status=sub.status,
|
288
|
-
tier=sub.tier,
|
289
|
-
monthly_price=Decimal(str(sub.monthly_price)),
|
290
|
-
usage_current=sub.usage_current,
|
291
|
-
usage_limit=sub.usage_limit,
|
292
|
-
usage_percentage=sub.usage_current / sub.usage_limit if sub.usage_limit else 0.0,
|
293
|
-
remaining_requests=sub.usage_limit - sub.usage_current if sub.usage_limit else 0,
|
294
|
-
expires_at=sub.expires_at,
|
295
|
-
next_billing=sub.next_billing,
|
296
|
-
created_at=sub.created_at
|
297
|
-
)
|
298
|
-
for sub in subscriptions
|
299
|
-
]
|
226
|
+
# Check endpoint group access
|
227
|
+
has_access = subscription.has_access_to_endpoint_group(endpoint_group)
|
300
228
|
|
229
|
+
if not has_access:
|
230
|
+
return self._create_error_result(
|
231
|
+
f"Access denied to endpoint group: {endpoint_group}",
|
232
|
+
"access_denied"
|
233
|
+
)
|
301
234
|
|
302
|
-
|
235
|
+
# Check rate limits
|
236
|
+
rate_limit_result = self._check_rate_limits(subscription)
|
237
|
+
if not rate_limit_result.success:
|
238
|
+
return rate_limit_result
|
239
|
+
|
240
|
+
return self._create_success_result(
|
241
|
+
"Access granted",
|
242
|
+
{
|
243
|
+
'user_id': user_id,
|
244
|
+
'endpoint_group': endpoint_group,
|
245
|
+
'subscription_id': str(subscription.id),
|
246
|
+
'tier': subscription.tier,
|
247
|
+
'requests_remaining': self._calculate_requests_remaining(subscription)
|
248
|
+
}
|
249
|
+
)
|
303
250
|
|
304
251
|
except Exception as e:
|
305
|
-
|
306
|
-
|
252
|
+
return self._handle_exception(
|
253
|
+
"check_access", e,
|
254
|
+
user_id=user_id,
|
255
|
+
endpoint_group=endpoint_group
|
256
|
+
)
|
307
257
|
|
308
|
-
def
|
309
|
-
self,
|
310
|
-
user: User,
|
311
|
-
subscription_id: str,
|
312
|
-
reason: str = 'user_request'
|
313
|
-
) -> SubscriptionResult:
|
258
|
+
def increment_usage(self, user_id: int) -> ServiceOperationResult:
|
314
259
|
"""
|
315
|
-
|
260
|
+
Increment subscription usage counter.
|
316
261
|
|
317
262
|
Args:
|
318
|
-
|
319
|
-
subscription_id: Subscription UUID
|
320
|
-
reason: Cancellation reason
|
263
|
+
user_id: User ID
|
321
264
|
|
322
265
|
Returns:
|
323
|
-
|
266
|
+
ServiceOperationResult: Usage increment result
|
324
267
|
"""
|
325
268
|
try:
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
success=True,
|
354
|
-
subscription_id=str(subscription.id)
|
269
|
+
# Get user subscription
|
270
|
+
subscription_result = self.get_user_subscription(user_id)
|
271
|
+
if not subscription_result.success:
|
272
|
+
return self._create_error_result(
|
273
|
+
subscription_result.message,
|
274
|
+
subscription_result.error_code
|
275
|
+
)
|
276
|
+
|
277
|
+
subscription = Subscription.objects.get(id=subscription_result.subscription_id)
|
278
|
+
|
279
|
+
# Increment usage using manager
|
280
|
+
success = subscription.increment_usage()
|
281
|
+
|
282
|
+
if success:
|
283
|
+
return self._create_success_result(
|
284
|
+
"Usage incremented successfully",
|
285
|
+
{
|
286
|
+
'user_id': user_id,
|
287
|
+
'subscription_id': str(subscription.id),
|
288
|
+
'total_requests': subscription.total_requests,
|
289
|
+
'requests_remaining': self._calculate_requests_remaining(subscription)
|
290
|
+
}
|
291
|
+
)
|
292
|
+
else:
|
293
|
+
return self._create_error_result(
|
294
|
+
"Failed to increment usage",
|
295
|
+
"increment_failed"
|
355
296
|
)
|
356
297
|
|
357
298
|
except Exception as e:
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
error_code='INTERNAL_ERROR',
|
362
|
-
error_message=f"Cancellation failed: {str(e)}"
|
299
|
+
return self._handle_exception(
|
300
|
+
"increment_usage", e,
|
301
|
+
user_id=user_id
|
363
302
|
)
|
364
303
|
|
365
|
-
def renew_subscription(
|
366
|
-
self,
|
367
|
-
subscription_id: str,
|
368
|
-
billing_period: Optional[str] = None
|
369
|
-
) -> SubscriptionResult:
|
304
|
+
def renew_subscription(self, subscription_id: str, duration_days: int = 30) -> SubscriptionResult:
|
370
305
|
"""
|
371
|
-
Renew
|
306
|
+
Renew existing subscription.
|
372
307
|
|
373
308
|
Args:
|
374
|
-
subscription_id: Subscription
|
375
|
-
|
309
|
+
subscription_id: Subscription ID
|
310
|
+
duration_days: Renewal duration in days
|
376
311
|
|
377
312
|
Returns:
|
378
|
-
SubscriptionResult
|
313
|
+
SubscriptionResult: Renewal result
|
379
314
|
"""
|
380
315
|
try:
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
subscription.next_billing = new_expiry
|
403
|
-
subscription.status = subscription.SubscriptionStatus.ACTIVE # Use proper enum
|
404
|
-
subscription.usage_current = 0 # Reset usage counter
|
405
|
-
subscription.save()
|
316
|
+
self.logger.info("Renewing subscription", extra={
|
317
|
+
'subscription_id': subscription_id,
|
318
|
+
'duration_days': duration_days
|
319
|
+
})
|
320
|
+
|
321
|
+
# Get subscription
|
322
|
+
try:
|
323
|
+
subscription = Subscription.objects.get(id=subscription_id)
|
324
|
+
except Subscription.DoesNotExist:
|
325
|
+
return SubscriptionResult(
|
326
|
+
success=False,
|
327
|
+
message=f"Subscription {subscription_id} not found",
|
328
|
+
error_code="subscription_not_found"
|
329
|
+
)
|
330
|
+
|
331
|
+
# Renew using manager
|
332
|
+
success = subscription.renew(duration_days)
|
333
|
+
|
334
|
+
if success:
|
335
|
+
subscription.refresh_from_db()
|
336
|
+
subscription_data = SubscriptionData.model_validate(subscription)
|
406
337
|
|
338
|
+
self._log_operation(
|
339
|
+
"renew_subscription",
|
340
|
+
True,
|
341
|
+
subscription_id=subscription_id,
|
342
|
+
duration_days=duration_days,
|
343
|
+
new_expires_at=subscription.expires_at.isoformat()
|
344
|
+
)
|
407
345
|
|
408
346
|
return SubscriptionResult(
|
409
347
|
success=True,
|
348
|
+
message="Subscription renewed successfully",
|
410
349
|
subscription_id=str(subscription.id),
|
411
|
-
|
350
|
+
user_id=subscription.user.id,
|
351
|
+
tier=subscription.tier,
|
352
|
+
status=subscription.status,
|
353
|
+
expires_at=subscription.expires_at,
|
354
|
+
data={'subscription': subscription_data.model_dump()}
|
355
|
+
)
|
356
|
+
else:
|
357
|
+
return SubscriptionResult(
|
358
|
+
success=False,
|
359
|
+
message="Failed to renew subscription",
|
360
|
+
error_code="renewal_failed"
|
412
361
|
)
|
413
362
|
|
414
363
|
except Exception as e:
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
error_message=f"Renewal failed: {str(e)}"
|
420
|
-
)
|
421
|
-
|
364
|
+
return SubscriptionResult(**self._handle_exception(
|
365
|
+
"renew_subscription", e,
|
366
|
+
subscription_id=subscription_id
|
367
|
+
).model_dump())
|
422
368
|
|
423
|
-
def
|
424
|
-
self,
|
425
|
-
user: User,
|
426
|
-
start_date: Optional[datetime] = None,
|
427
|
-
end_date: Optional[datetime] = None
|
428
|
-
) -> Dict[str, Any]:
|
369
|
+
def cancel_subscription(self, subscription_id: str, reason: str = None) -> SubscriptionResult:
|
429
370
|
"""
|
430
|
-
|
371
|
+
Cancel subscription.
|
431
372
|
|
432
373
|
Args:
|
433
|
-
|
434
|
-
|
435
|
-
end_date: Analytics end date
|
374
|
+
subscription_id: Subscription ID
|
375
|
+
reason: Cancellation reason
|
436
376
|
|
437
377
|
Returns:
|
438
|
-
|
378
|
+
SubscriptionResult: Cancellation result
|
439
379
|
"""
|
440
380
|
try:
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
# Get
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
active_subscriptions = subscriptions.filter(
|
456
|
-
status=Subscription.Status.ACTIVE,
|
457
|
-
expires_at__gt=timezone.now()
|
458
|
-
).count()
|
381
|
+
self.logger.info("Cancelling subscription", extra={
|
382
|
+
'subscription_id': subscription_id,
|
383
|
+
'reason': reason
|
384
|
+
})
|
385
|
+
|
386
|
+
# Get subscription
|
387
|
+
try:
|
388
|
+
subscription = Subscription.objects.get(id=subscription_id)
|
389
|
+
except Subscription.DoesNotExist:
|
390
|
+
return SubscriptionResult(
|
391
|
+
success=False,
|
392
|
+
message=f"Subscription {subscription_id} not found",
|
393
|
+
error_code="subscription_not_found"
|
394
|
+
)
|
459
395
|
|
460
|
-
|
461
|
-
|
462
|
-
endpoint_name = sub.endpoint_group.name
|
463
|
-
if endpoint_name not in usage_by_endpoint:
|
464
|
-
usage_by_endpoint[endpoint_name] = {
|
465
|
-
'usage': 0,
|
466
|
-
'limit': 0,
|
467
|
-
'percentage': 0
|
468
|
-
}
|
469
|
-
usage_by_endpoint[endpoint_name]['usage'] += sub.current_usage
|
470
|
-
usage_by_endpoint[endpoint_name]['limit'] += sub.get_monthly_limit()
|
471
|
-
|
472
|
-
# Calculate usage percentages
|
473
|
-
for endpoint_data in usage_by_endpoint.values():
|
474
|
-
if endpoint_data['limit'] > 0:
|
475
|
-
endpoint_data['percentage'] = (endpoint_data['usage'] / endpoint_data['limit']) * 100
|
476
|
-
|
477
|
-
return {
|
478
|
-
'period': {
|
479
|
-
'start_date': start_date.isoformat(),
|
480
|
-
'end_date': end_date.isoformat()
|
481
|
-
},
|
482
|
-
'summary': {
|
483
|
-
'total_subscriptions': total_subscriptions,
|
484
|
-
'active_subscriptions': active_subscriptions,
|
485
|
-
'cancelled_subscriptions': subscriptions.filter(
|
486
|
-
status=Subscription.Status.CANCELLED
|
487
|
-
).count()
|
488
|
-
},
|
489
|
-
'usage_by_endpoint': usage_by_endpoint,
|
490
|
-
'total_usage': sum(data['usage'] for data in usage_by_endpoint.values()),
|
491
|
-
'total_limit': sum(data['limit'] for data in usage_by_endpoint.values())
|
492
|
-
}
|
396
|
+
# Cancel using manager
|
397
|
+
success = subscription.cancel(reason)
|
493
398
|
|
399
|
+
if success:
|
400
|
+
subscription.refresh_from_db()
|
401
|
+
subscription_data = SubscriptionData.model_validate(subscription)
|
402
|
+
|
403
|
+
self._log_operation(
|
404
|
+
"cancel_subscription",
|
405
|
+
True,
|
406
|
+
subscription_id=subscription_id,
|
407
|
+
reason=reason
|
408
|
+
)
|
409
|
+
|
410
|
+
return SubscriptionResult(
|
411
|
+
success=True,
|
412
|
+
message="Subscription cancelled successfully",
|
413
|
+
subscription_id=str(subscription.id),
|
414
|
+
user_id=subscription.user.id,
|
415
|
+
tier=subscription.tier,
|
416
|
+
status=subscription.status,
|
417
|
+
data={'subscription': subscription_data.model_dump()}
|
418
|
+
)
|
419
|
+
else:
|
420
|
+
return SubscriptionResult(
|
421
|
+
success=False,
|
422
|
+
message="Failed to cancel subscription",
|
423
|
+
error_code="cancellation_failed"
|
424
|
+
)
|
425
|
+
|
494
426
|
except Exception as e:
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
'start_date': start_date.isoformat() if start_date else None,
|
500
|
-
'end_date': end_date.isoformat() if end_date else None
|
501
|
-
}
|
502
|
-
}
|
427
|
+
return SubscriptionResult(**self._handle_exception(
|
428
|
+
"cancel_subscription", e,
|
429
|
+
subscription_id=subscription_id
|
430
|
+
).model_dump())
|
503
431
|
|
504
|
-
def
|
505
|
-
self,
|
506
|
-
user_id: int,
|
507
|
-
endpoint_group: str,
|
508
|
-
increment_usage: bool = False
|
509
|
-
) -> Dict[str, Any]:
|
432
|
+
def get_subscription_stats(self, days: int = 30) -> ServiceOperationResult:
|
510
433
|
"""
|
511
|
-
|
434
|
+
Get subscription statistics.
|
512
435
|
|
513
436
|
Args:
|
514
|
-
|
515
|
-
endpoint_group: Endpoint group name
|
516
|
-
increment_usage: Whether to increment usage count
|
437
|
+
days: Number of days to analyze
|
517
438
|
|
518
439
|
Returns:
|
519
|
-
|
440
|
+
ServiceOperationResult: Subscription statistics
|
520
441
|
"""
|
521
442
|
try:
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
443
|
+
since = timezone.now() - timedelta(days=days)
|
444
|
+
|
445
|
+
# Overall stats
|
446
|
+
overall_stats = Subscription.objects.aggregate(
|
447
|
+
total_subscriptions=models.Count('id'),
|
448
|
+
active_subscriptions=models.Count(
|
449
|
+
'id',
|
450
|
+
filter=models.Q(status=Subscription.SubscriptionStatus.ACTIVE)
|
451
|
+
),
|
452
|
+
expired_subscriptions=models.Count(
|
453
|
+
'id',
|
454
|
+
filter=models.Q(status=Subscription.SubscriptionStatus.EXPIRED)
|
455
|
+
),
|
456
|
+
cancelled_subscriptions=models.Count(
|
457
|
+
'id',
|
458
|
+
filter=models.Q(status=Subscription.SubscriptionStatus.CANCELLED)
|
459
|
+
)
|
527
460
|
)
|
528
461
|
|
529
|
-
#
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
'
|
534
|
-
|
535
|
-
|
536
|
-
|
462
|
+
# Tier breakdown
|
463
|
+
tier_breakdown = Subscription.objects.values('tier').annotate(
|
464
|
+
count=models.Count('id'),
|
465
|
+
active_count=models.Count(
|
466
|
+
'id',
|
467
|
+
filter=models.Q(status=Subscription.SubscriptionStatus.ACTIVE)
|
468
|
+
)
|
469
|
+
).order_by('-count')
|
470
|
+
|
471
|
+
# Recent activity
|
472
|
+
recent_stats = Subscription.objects.filter(
|
473
|
+
created_at__gte=since
|
474
|
+
).aggregate(
|
475
|
+
new_subscriptions=models.Count('id'),
|
476
|
+
total_requests=models.Sum('total_requests'),
|
477
|
+
avg_requests=models.Avg('total_requests')
|
478
|
+
)
|
537
479
|
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
'has_access': True,
|
545
|
-
'subscription_id': str(subscription.id),
|
546
|
-
'usage_current': subscription.usage_current,
|
547
|
-
'usage_limit': subscription.usage_limit,
|
548
|
-
'remaining_requests': subscription.usage_limit - subscription.usage_current if subscription.usage_limit else None
|
480
|
+
stats = {
|
481
|
+
'period_days': days,
|
482
|
+
'overall_stats': overall_stats,
|
483
|
+
'tier_breakdown': list(tier_breakdown),
|
484
|
+
'recent_stats': recent_stats,
|
485
|
+
'generated_at': timezone.now().isoformat()
|
549
486
|
}
|
550
487
|
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
488
|
+
return self._create_success_result(
|
489
|
+
f"Subscription statistics for {days} days",
|
490
|
+
stats
|
491
|
+
)
|
492
|
+
|
556
493
|
except Exception as e:
|
557
|
-
|
558
|
-
return {
|
559
|
-
'has_access': False,
|
560
|
-
'reason': 'internal_error'
|
561
|
-
}
|
494
|
+
return self._handle_exception("get_subscription_stats", e)
|
562
495
|
|
563
|
-
def
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
496
|
+
def _get_default_endpoint_groups(self, tier: str) -> List[EndpointGroup]:
|
497
|
+
"""Get default endpoint groups for subscription tier."""
|
498
|
+
tier_groups = {
|
499
|
+
'free': ['payments', 'balance'],
|
500
|
+
'basic': ['payments', 'balance', 'subscriptions'],
|
501
|
+
'pro': ['payments', 'balance', 'subscriptions', 'analytics'],
|
502
|
+
'enterprise': ['payments', 'balance', 'subscriptions', 'analytics', 'admin']
|
503
|
+
}
|
571
504
|
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
"""
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
505
|
+
group_codes = tier_groups.get(tier, ['payments'])
|
506
|
+
return EndpointGroup.objects.filter(
|
507
|
+
code__in=group_codes,
|
508
|
+
is_enabled=True
|
509
|
+
)
|
510
|
+
|
511
|
+
def _calculate_requests_remaining(self, subscription: Subscription) -> int:
|
512
|
+
"""Calculate remaining requests for today."""
|
513
|
+
# Simple calculation - in production this would check daily usage
|
514
|
+
return max(0, subscription.requests_per_day - subscription.total_requests)
|
515
|
+
|
516
|
+
def _check_rate_limits(self, subscription: Subscription) -> ServiceOperationResult:
|
517
|
+
"""Check if subscription has exceeded rate limits."""
|
518
|
+
# Simplified rate limit check
|
519
|
+
if subscription.total_requests >= subscription.requests_per_day:
|
520
|
+
return self._create_error_result(
|
521
|
+
"Daily request limit exceeded",
|
522
|
+
"rate_limit_exceeded"
|
586
523
|
)
|
524
|
+
|
525
|
+
return self._create_success_result("Rate limits OK")
|
526
|
+
|
527
|
+
def health_check(self) -> ServiceOperationResult:
|
528
|
+
"""Perform subscription service health check."""
|
529
|
+
try:
|
530
|
+
# Check database connectivity
|
531
|
+
subscription_count = Subscription.objects.count()
|
532
|
+
active_count = Subscription.objects.filter(
|
533
|
+
status=Subscription.SubscriptionStatus.ACTIVE
|
534
|
+
).count()
|
587
535
|
|
588
|
-
|
589
|
-
|
536
|
+
# Check for expired subscriptions that need cleanup
|
537
|
+
expired_count = Subscription.objects.filter(
|
538
|
+
status=Subscription.SubscriptionStatus.ACTIVE,
|
539
|
+
expires_at__lt=timezone.now()
|
540
|
+
).count()
|
590
541
|
|
542
|
+
stats = {
|
543
|
+
'total_subscriptions': subscription_count,
|
544
|
+
'active_subscriptions': active_count,
|
545
|
+
'expired_needing_cleanup': expired_count,
|
546
|
+
'service_name': 'SubscriptionService'
|
547
|
+
}
|
591
548
|
|
592
|
-
return
|
593
|
-
|
594
|
-
|
595
|
-
'usage_current': subscription.usage_current,
|
596
|
-
'usage_limit': subscription.usage_limit,
|
597
|
-
'remaining_requests': subscription.usage_limit - subscription.usage_current if subscription.usage_limit else None
|
598
|
-
}
|
549
|
+
return self._create_success_result(
|
550
|
+
"SubscriptionService is healthy",
|
551
|
+
stats
|
599
552
|
)
|
600
553
|
|
601
|
-
except Subscription.DoesNotExist:
|
602
|
-
return ServiceOperationResult(
|
603
|
-
success=False,
|
604
|
-
error_message='no_active_subscription'
|
605
|
-
)
|
606
554
|
except Exception as e:
|
607
|
-
|
608
|
-
return ServiceOperationResult(
|
609
|
-
success=False,
|
610
|
-
error_message='internal_error'
|
611
|
-
)
|
555
|
+
return self._handle_exception("health_check", e)
|