django-cfg 1.2.29__py3-none-any.whl → 1.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -9
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +600 -108
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +470 -64
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +381 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +343 -163
- django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +16 -20
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +207 -67
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -284
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +344 -468
- django_cfg/apps/payments/services/core/subscription_service.py +425 -484
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +232 -71
- django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
- django_cfg/apps/payments/services/providers/registry.py +429 -80
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +211 -130
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +129 -98
- django_cfg/apps/payments/signals/subscription_signals.py +195 -143
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +46 -47
- django_cfg/apps/payments/urls_admin.py +49 -0
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +75 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -498
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +65 -12
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -178
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/managers/__init__.py +0 -22
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -83
- django_cfg/apps/payments/managers/payment_manager.py +0 -44
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -56
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -55
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -297
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -637
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/urls_templates.py +0 -52
- django_cfg/apps/payments/utils/__init__.py +0 -45
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -245
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -62
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -111
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -312
- django_cfg/apps/payments/views/templates/base.py +0 -204
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -164
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -240
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -65
- django_cfg/core/integration.py +0 -160
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,614 +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
|
-
import
|
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
|
-
|
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
|
20
|
-
|
21
|
-
User = get_user_model()
|
22
|
-
logger = logging.getLogger(__name__)
|
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
19
|
|
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
20
|
|
44
|
-
|
45
|
-
class AccessCheck(BaseModel):
|
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
|
-
from ..internal_types import SubscriptionInfo, EndpointGroupInfo
|
280
|
-
from decimal import Decimal
|
281
|
-
|
282
|
-
result = [
|
283
|
-
SubscriptionInfo(
|
284
|
-
id=str(sub.id),
|
285
|
-
endpoint_group=EndpointGroupInfo(
|
286
|
-
id=str(sub.endpoint_group.id),
|
287
|
-
name=sub.endpoint_group.name,
|
288
|
-
display_name=sub.endpoint_group.display_name
|
289
|
-
),
|
290
|
-
status=sub.status,
|
291
|
-
tier=sub.tier,
|
292
|
-
monthly_price=Decimal(str(sub.monthly_price)),
|
293
|
-
usage_current=sub.usage_current,
|
294
|
-
usage_limit=sub.usage_limit,
|
295
|
-
usage_percentage=sub.usage_current / sub.usage_limit if sub.usage_limit else 0.0,
|
296
|
-
remaining_requests=sub.usage_limit - sub.usage_current if sub.usage_limit else 0,
|
297
|
-
expires_at=sub.expires_at,
|
298
|
-
next_billing=sub.next_billing,
|
299
|
-
created_at=sub.created_at
|
300
|
-
)
|
301
|
-
for sub in subscriptions
|
302
|
-
]
|
226
|
+
# Check endpoint group access
|
227
|
+
has_access = subscription.has_access_to_endpoint_group(endpoint_group)
|
303
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
|
+
)
|
304
234
|
|
305
|
-
|
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
|
+
)
|
306
250
|
|
307
251
|
except Exception as e:
|
308
|
-
|
309
|
-
|
252
|
+
return self._handle_exception(
|
253
|
+
"check_access", e,
|
254
|
+
user_id=user_id,
|
255
|
+
endpoint_group=endpoint_group
|
256
|
+
)
|
310
257
|
|
311
|
-
def
|
312
|
-
self,
|
313
|
-
user: User,
|
314
|
-
subscription_id: str,
|
315
|
-
reason: str = 'user_request'
|
316
|
-
) -> SubscriptionResult:
|
258
|
+
def increment_usage(self, user_id: int) -> ServiceOperationResult:
|
317
259
|
"""
|
318
|
-
|
260
|
+
Increment subscription usage counter.
|
319
261
|
|
320
262
|
Args:
|
321
|
-
|
322
|
-
subscription_id: Subscription UUID
|
323
|
-
reason: Cancellation reason
|
263
|
+
user_id: User ID
|
324
264
|
|
325
265
|
Returns:
|
326
|
-
|
266
|
+
ServiceOperationResult: Usage increment result
|
327
267
|
"""
|
328
268
|
try:
|
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
|
-
|
354
|
-
|
355
|
-
|
356
|
-
success=True,
|
357
|
-
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"
|
358
296
|
)
|
359
297
|
|
360
298
|
except Exception as e:
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
error_code='INTERNAL_ERROR',
|
365
|
-
error_message=f"Cancellation failed: {str(e)}"
|
299
|
+
return self._handle_exception(
|
300
|
+
"increment_usage", e,
|
301
|
+
user_id=user_id
|
366
302
|
)
|
367
303
|
|
368
|
-
def renew_subscription(
|
369
|
-
self,
|
370
|
-
subscription_id: str,
|
371
|
-
billing_period: Optional[str] = None
|
372
|
-
) -> SubscriptionResult:
|
304
|
+
def renew_subscription(self, subscription_id: str, duration_days: int = 30) -> SubscriptionResult:
|
373
305
|
"""
|
374
|
-
Renew
|
306
|
+
Renew existing subscription.
|
375
307
|
|
376
308
|
Args:
|
377
|
-
subscription_id: Subscription
|
378
|
-
|
309
|
+
subscription_id: Subscription ID
|
310
|
+
duration_days: Renewal duration in days
|
379
311
|
|
380
312
|
Returns:
|
381
|
-
SubscriptionResult
|
313
|
+
SubscriptionResult: Renewal result
|
382
314
|
"""
|
383
315
|
try:
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
subscription.next_billing = new_expiry
|
406
|
-
subscription.status = subscription.SubscriptionStatus.ACTIVE # Use proper enum
|
407
|
-
subscription.usage_current = 0 # Reset usage counter
|
408
|
-
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)
|
409
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
|
+
)
|
410
345
|
|
411
346
|
return SubscriptionResult(
|
412
347
|
success=True,
|
348
|
+
message="Subscription renewed successfully",
|
413
349
|
subscription_id=str(subscription.id),
|
414
|
-
|
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"
|
415
361
|
)
|
416
362
|
|
417
363
|
except Exception as e:
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
error_message=f"Renewal failed: {str(e)}"
|
423
|
-
)
|
364
|
+
return SubscriptionResult(**self._handle_exception(
|
365
|
+
"renew_subscription", e,
|
366
|
+
subscription_id=subscription_id
|
367
|
+
).model_dump())
|
424
368
|
|
425
|
-
|
426
|
-
def get_subscription_analytics(
|
427
|
-
self,
|
428
|
-
user: User,
|
429
|
-
start_date: Optional[datetime] = None,
|
430
|
-
end_date: Optional[datetime] = None
|
431
|
-
) -> Dict[str, Any]:
|
369
|
+
def cancel_subscription(self, subscription_id: str, reason: str = None) -> SubscriptionResult:
|
432
370
|
"""
|
433
|
-
|
371
|
+
Cancel subscription.
|
434
372
|
|
435
373
|
Args:
|
436
|
-
|
437
|
-
|
438
|
-
end_date: Analytics end date
|
374
|
+
subscription_id: Subscription ID
|
375
|
+
reason: Cancellation reason
|
439
376
|
|
440
377
|
Returns:
|
441
|
-
|
378
|
+
SubscriptionResult: Cancellation result
|
442
379
|
"""
|
443
380
|
try:
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
# Get
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
active_subscriptions = subscriptions.filter(
|
459
|
-
status=Subscription.Status.ACTIVE,
|
460
|
-
expires_at__gt=timezone.now()
|
461
|
-
).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
|
+
)
|
462
395
|
|
463
|
-
|
464
|
-
|
465
|
-
endpoint_name = sub.endpoint_group.name
|
466
|
-
if endpoint_name not in usage_by_endpoint:
|
467
|
-
usage_by_endpoint[endpoint_name] = {
|
468
|
-
'usage': 0,
|
469
|
-
'limit': 0,
|
470
|
-
'percentage': 0
|
471
|
-
}
|
472
|
-
usage_by_endpoint[endpoint_name]['usage'] += sub.current_usage
|
473
|
-
usage_by_endpoint[endpoint_name]['limit'] += sub.get_monthly_limit()
|
474
|
-
|
475
|
-
# Calculate usage percentages
|
476
|
-
for endpoint_data in usage_by_endpoint.values():
|
477
|
-
if endpoint_data['limit'] > 0:
|
478
|
-
endpoint_data['percentage'] = (endpoint_data['usage'] / endpoint_data['limit']) * 100
|
479
|
-
|
480
|
-
return {
|
481
|
-
'period': {
|
482
|
-
'start_date': start_date.isoformat(),
|
483
|
-
'end_date': end_date.isoformat()
|
484
|
-
},
|
485
|
-
'summary': {
|
486
|
-
'total_subscriptions': total_subscriptions,
|
487
|
-
'active_subscriptions': active_subscriptions,
|
488
|
-
'cancelled_subscriptions': subscriptions.filter(
|
489
|
-
status=Subscription.Status.CANCELLED
|
490
|
-
).count()
|
491
|
-
},
|
492
|
-
'usage_by_endpoint': usage_by_endpoint,
|
493
|
-
'total_usage': sum(data['usage'] for data in usage_by_endpoint.values()),
|
494
|
-
'total_limit': sum(data['limit'] for data in usage_by_endpoint.values())
|
495
|
-
}
|
396
|
+
# Cancel using manager
|
397
|
+
success = subscription.cancel(reason)
|
496
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
|
+
|
497
426
|
except Exception as e:
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
'start_date': start_date.isoformat() if start_date else None,
|
503
|
-
'end_date': end_date.isoformat() if end_date else None
|
504
|
-
}
|
505
|
-
}
|
427
|
+
return SubscriptionResult(**self._handle_exception(
|
428
|
+
"cancel_subscription", e,
|
429
|
+
subscription_id=subscription_id
|
430
|
+
).model_dump())
|
506
431
|
|
507
|
-
def
|
508
|
-
self,
|
509
|
-
user_id: int,
|
510
|
-
endpoint_group: str,
|
511
|
-
increment_usage: bool = False
|
512
|
-
) -> Dict[str, Any]:
|
432
|
+
def get_subscription_stats(self, days: int = 30) -> ServiceOperationResult:
|
513
433
|
"""
|
514
|
-
|
434
|
+
Get subscription statistics.
|
515
435
|
|
516
436
|
Args:
|
517
|
-
|
518
|
-
endpoint_group: Endpoint group name
|
519
|
-
increment_usage: Whether to increment usage count
|
437
|
+
days: Number of days to analyze
|
520
438
|
|
521
439
|
Returns:
|
522
|
-
|
440
|
+
ServiceOperationResult: Subscription statistics
|
523
441
|
"""
|
524
442
|
try:
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
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
|
+
)
|
530
460
|
)
|
531
461
|
|
532
|
-
#
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
'
|
537
|
-
|
538
|
-
|
539
|
-
|
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
|
+
)
|
540
479
|
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
'has_access': True,
|
548
|
-
'subscription_id': str(subscription.id),
|
549
|
-
'usage_current': subscription.usage_current,
|
550
|
-
'usage_limit': subscription.usage_limit,
|
551
|
-
'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()
|
552
486
|
}
|
553
487
|
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
488
|
+
return self._create_success_result(
|
489
|
+
f"Subscription statistics for {days} days",
|
490
|
+
stats
|
491
|
+
)
|
492
|
+
|
559
493
|
except Exception as e:
|
560
|
-
|
561
|
-
return {
|
562
|
-
'has_access': False,
|
563
|
-
'reason': 'internal_error'
|
564
|
-
}
|
494
|
+
return self._handle_exception("get_subscription_stats", e)
|
565
495
|
|
566
|
-
def
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
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
|
+
}
|
574
504
|
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
"""
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
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"
|
589
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()
|
590
535
|
|
591
|
-
|
592
|
-
|
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()
|
593
541
|
|
542
|
+
stats = {
|
543
|
+
'total_subscriptions': subscription_count,
|
544
|
+
'active_subscriptions': active_count,
|
545
|
+
'expired_needing_cleanup': expired_count,
|
546
|
+
'service_name': 'SubscriptionService'
|
547
|
+
}
|
594
548
|
|
595
|
-
return
|
596
|
-
|
597
|
-
|
598
|
-
'usage_current': subscription.usage_current,
|
599
|
-
'usage_limit': subscription.usage_limit,
|
600
|
-
'remaining_requests': subscription.usage_limit - subscription.usage_current if subscription.usage_limit else None
|
601
|
-
}
|
549
|
+
return self._create_success_result(
|
550
|
+
"SubscriptionService is healthy",
|
551
|
+
stats
|
602
552
|
)
|
603
553
|
|
604
|
-
except Subscription.DoesNotExist:
|
605
|
-
return ServiceOperationResult(
|
606
|
-
success=False,
|
607
|
-
error_message='no_active_subscription'
|
608
|
-
)
|
609
554
|
except Exception as e:
|
610
|
-
|
611
|
-
return ServiceOperationResult(
|
612
|
-
success=False,
|
613
|
-
error_message='internal_error'
|
614
|
-
)
|
555
|
+
return self._handle_exception("health_check", e)
|