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.
Files changed (256) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/health/views.py +4 -2
  3. django_cfg/apps/knowbase/config/settings.py +16 -15
  4. django_cfg/apps/payments/README.md +326 -0
  5. django_cfg/apps/payments/admin/__init__.py +20 -10
  6. django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
  7. django_cfg/apps/payments/admin/balance_admin.py +592 -297
  8. django_cfg/apps/payments/admin/currencies_admin.py +526 -222
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +465 -70
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
  39. django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
  43. django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +13 -18
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +172 -148
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -285
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +346 -467
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -481
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +234 -174
  74. django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
  75. django_cfg/apps/payments/services/providers/registry.py +367 -301
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +210 -129
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +128 -103
  85. django_cfg/apps/payments/signals/subscription_signals.py +194 -142
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +45 -48
  93. django_cfg/apps/payments/urls_admin.py +33 -42
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/config.py +1 -1
  110. django_cfg/core/config.py +40 -4
  111. django_cfg/core/generation.py +25 -4
  112. django_cfg/core/integration/README.md +363 -0
  113. django_cfg/core/integration/__init__.py +47 -0
  114. django_cfg/core/integration/commands_collector.py +239 -0
  115. django_cfg/core/integration/display/__init__.py +15 -0
  116. django_cfg/core/integration/display/base.py +157 -0
  117. django_cfg/core/integration/display/ngrok.py +164 -0
  118. django_cfg/core/integration/display/startup.py +815 -0
  119. django_cfg/core/integration/url_integration.py +123 -0
  120. django_cfg/core/integration/version_checker.py +160 -0
  121. django_cfg/management/commands/auto_generate.py +4 -0
  122. django_cfg/management/commands/check_settings.py +6 -0
  123. django_cfg/management/commands/clear_constance.py +5 -2
  124. django_cfg/management/commands/create_token.py +6 -0
  125. django_cfg/management/commands/list_urls.py +6 -0
  126. django_cfg/management/commands/migrate_all.py +6 -0
  127. django_cfg/management/commands/migrator.py +3 -0
  128. django_cfg/management/commands/rundramatiq.py +6 -0
  129. django_cfg/management/commands/runserver_ngrok.py +51 -29
  130. django_cfg/management/commands/script.py +6 -0
  131. django_cfg/management/commands/show_config.py +12 -2
  132. django_cfg/management/commands/show_urls.py +4 -0
  133. django_cfg/management/commands/superuser.py +6 -0
  134. django_cfg/management/commands/task_clear.py +4 -1
  135. django_cfg/management/commands/task_status.py +3 -1
  136. django_cfg/management/commands/test_email.py +3 -0
  137. django_cfg/management/commands/test_telegram.py +6 -0
  138. django_cfg/management/commands/test_twilio.py +6 -0
  139. django_cfg/management/commands/tree.py +6 -0
  140. django_cfg/management/commands/validate_config.py +155 -149
  141. django_cfg/models/constance.py +31 -11
  142. django_cfg/models/payments.py +175 -492
  143. django_cfg/modules/django_logger.py +160 -146
  144. django_cfg/modules/django_unfold/dashboard.py +64 -16
  145. django_cfg/registry/core.py +1 -0
  146. django_cfg/template_archive/django_sample.zip +0 -0
  147. django_cfg/utils/smart_defaults.py +222 -571
  148. django_cfg/utils/toolkit.py +51 -11
  149. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
  150. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
  151. django_cfg/apps/payments/__init__.py +0 -8
  152. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  153. django_cfg/apps/payments/config/module.py +0 -70
  154. django_cfg/apps/payments/config/providers.py +0 -105
  155. django_cfg/apps/payments/config/settings.py +0 -96
  156. django_cfg/apps/payments/config/utils.py +0 -52
  157. django_cfg/apps/payments/decorators.py +0 -291
  158. django_cfg/apps/payments/management/commands/README.md +0 -146
  159. django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
  160. django_cfg/apps/payments/managers/__init__.py +0 -23
  161. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  162. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  163. django_cfg/apps/payments/managers/currency_manager.py +0 -306
  164. django_cfg/apps/payments/managers/payment_manager.py +0 -192
  165. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  166. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  167. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
  168. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
  169. django_cfg/apps/payments/models/events.py +0 -73
  170. django_cfg/apps/payments/serializers/__init__.py +0 -57
  171. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  172. django_cfg/apps/payments/serializers/balance.py +0 -59
  173. django_cfg/apps/payments/serializers/currencies.py +0 -63
  174. django_cfg/apps/payments/serializers/payments.py +0 -62
  175. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  176. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  177. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  178. django_cfg/apps/payments/services/cache/base.py +0 -30
  179. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  180. django_cfg/apps/payments/services/internal_types.py +0 -461
  181. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  182. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  183. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
  184. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  185. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
  186. django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
  187. django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
  188. django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
  189. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
  190. django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
  191. django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
  192. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
  193. django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
  194. django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
  195. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
  196. django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
  197. django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
  198. django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
  199. django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
  200. django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
  201. django_cfg/apps/payments/services/security/__init__.py +0 -34
  202. django_cfg/apps/payments/services/security/error_handler.py +0 -635
  203. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  204. django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
  205. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  206. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  207. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  208. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  209. django_cfg/apps/payments/tasks/__init__.py +0 -12
  210. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  211. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
  212. django_cfg/apps/payments/templates/payments/base.html +0 -182
  213. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  214. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  215. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
  216. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  217. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
  218. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
  219. django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
  220. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
  221. django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
  222. django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
  223. django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
  224. django_cfg/apps/payments/templates/payments/stats.html +0 -261
  225. django_cfg/apps/payments/templates/payments/test.html +0 -213
  226. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  227. django_cfg/apps/payments/utils/__init__.py +0 -43
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -239
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -63
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -122
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -451
  241. django_cfg/apps/payments/views/templates/base.py +0 -212
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -158
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -244
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -66
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/template_archive/.gitignore +0 -1
  252. django_cfg/template_archive/__init__.py +0 -0
  253. django_cfg/urls.py +0 -33
  254. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  255. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  256. {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 Service - Core subscription management and access control.
2
+ Subscription service for the Universal Payment System v2.0.
3
3
 
4
- This service handles subscription creation, renewal, access validation,
5
- and usage tracking with Redis caching.
4
+ Handles subscription management and access control.
6
5
  """
7
6
 
8
- from typing import Dict, Any, Optional, List
9
- from django_cfg.modules.django_logger import get_logger
10
- from datetime import datetime, timedelta, timezone as dt_timezone
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 pydantic import BaseModel, Field, ValidationError
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 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
- Universal subscription management service.
23
+ Subscription service with business logic and validation.
60
24
 
61
- Handles subscription lifecycle, access control, and usage tracking
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 __init__(self):
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
- subscription_data: Dictionary with subscription details
33
+ request: Subscription creation request with validation
75
34
 
76
35
  Returns:
77
- ServiceOperationResult with subscription details
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
- user = User.objects.get(id=subscription_data['user_id'])
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
- # Get endpoint group
84
- endpoint_group = EndpointGroup.objects.get(
85
- name=subscription_data['endpoint_group_name'],
86
- is_active=True
69
+ # Cancel existing active subscriptions
70
+ existing_active = Subscription.objects.filter(
71
+ user=user,
72
+ status=Subscription.SubscriptionStatus.ACTIVE
87
73
  )
88
74
 
89
- with transaction.atomic():
90
- # Check for existing active subscription
91
- existing = Subscription.objects.filter(
92
- user=user,
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
- if existing:
99
- return ServiceOperationResult(
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
- endpoint_group=endpoint_group,
108
- tier=Subscription.SubscriptionTier.BASIC,
85
+ tier=request.tier,
109
86
  status=Subscription.SubscriptionStatus.ACTIVE,
110
- monthly_price=endpoint_group.basic_price,
111
- usage_limit=endpoint_group.basic_limit,
112
- usage_current=0,
113
- expires_at=timezone.now() + timedelta(days=30),
114
- next_billing=timezone.now() + timedelta(days=30)
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
- # Log subscription creation
118
- logger.info(
119
- f"New subscription created: {subscription_data['endpoint_group_name']} "
120
- f"for user {user.email} (expires: {subscription.expires_at})"
121
- )
122
-
123
-
124
- return ServiceOperationResult(
125
- success=True,
126
- data={'subscription_id': str(subscription.id)}
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
- except Exception as e:
130
- logger.error(f"Subscription creation failed: {e}", exc_info=True)
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 ServiceOperationResult(
133
- success=False,
134
- error_message=f"Internal error: {str(e)}"
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 check_endpoint_access(
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
- Check if user has access to endpoint group.
140
+ Get active subscription for user.
145
141
 
146
142
  Args:
147
- user: User object
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
- AccessCheck with access status and details
146
+ SubscriptionResult: Active subscription or free tier
153
147
  """
154
148
  try:
155
- # Try cache first
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
- if not subscription:
171
- result = AccessCheck(
172
- allowed=False,
173
- reason='no_active_subscription',
174
- required_subscription=endpoint_group_name
175
- )
176
- elif not subscription.can_make_request():
177
- result = AccessCheck(
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
- # Cache result for 1 minute
193
- if use_cache:
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
- return result
164
+ if not subscription:
165
+ # Create free subscription if none exists
166
+ subscription = Subscription.objects.create_free_subscription(user)
198
167
 
199
- except Exception as e:
200
- logger.error(f"Access check failed for user {user.id}, endpoint {endpoint_group_name}: {e}")
201
- return AccessCheck(
202
- allowed=False,
203
- reason='check_failed'
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
- logger.error(f"Usage recording failed for user {user.id}: {e}")
247
- return False
187
+ return SubscriptionResult(**self._handle_exception(
188
+ "get_user_subscription", e,
189
+ user_id=user_id
190
+ ).model_dump())
248
191
 
249
- def get_user_subscriptions(
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
- Get user's subscriptions.
194
+ Check if user has access to endpoint group.
256
195
 
257
196
  Args:
258
197
  user_id: User ID
259
- active_only: Return only active subscriptions
198
+ endpoint_group: Endpoint group code
260
199
 
261
200
  Returns:
262
- List of subscription dictionaries
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
- # Query subscriptions
267
- queryset = Subscription.objects.filter(user_id=user_id)
217
+ subscription = Subscription.objects.get(id=subscription_result.subscription_id)
268
218
 
269
- if active_only:
270
- queryset = queryset.filter(
271
- status=Subscription.SubscriptionStatus.ACTIVE,
272
- expires_at__gt=timezone.now()
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
- subscriptions = queryset.select_related(
276
- 'endpoint_group'
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
- return result
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
- logger.error(f"Error getting subscriptions for user {user_id}: {e}")
306
- return []
252
+ return self._handle_exception(
253
+ "check_access", e,
254
+ user_id=user_id,
255
+ endpoint_group=endpoint_group
256
+ )
307
257
 
308
- def cancel_subscription(
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
- Cancel user subscription.
260
+ Increment subscription usage counter.
316
261
 
317
262
  Args:
318
- user: User object
319
- subscription_id: Subscription UUID
320
- reason: Cancellation reason
263
+ user_id: User ID
321
264
 
322
265
  Returns:
323
- SubscriptionResult with cancellation status
266
+ ServiceOperationResult: Usage increment result
324
267
  """
325
268
  try:
326
- with transaction.atomic():
327
- subscription = Subscription.objects.filter(
328
- id=subscription_id,
329
- user=user,
330
- status=Subscription.Status.ACTIVE
331
- ).first()
332
-
333
- if not subscription:
334
- return SubscriptionResult(
335
- success=False,
336
- error_code='SUBSCRIPTION_NOT_FOUND',
337
- error_message="Active subscription not found"
338
- )
339
-
340
- # Cancel subscription
341
- subscription.status = Subscription.Status.CANCELLED
342
- subscription.auto_renew = False
343
- subscription.next_billing_at = None
344
- subscription.metadata = {
345
- **subscription.metadata,
346
- 'cancellation_reason': reason,
347
- 'cancelled_at': timezone.now().isoformat()
348
- }
349
- subscription.save()
350
-
351
-
352
- return SubscriptionResult(
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
- logger.error(f"Subscription cancellation failed: {e}", exc_info=True)
359
- return SubscriptionResult(
360
- success=False,
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 expired or expiring subscription.
306
+ Renew existing subscription.
372
307
 
373
308
  Args:
374
- subscription_id: Subscription UUID
375
- billing_period: New billing period (optional)
309
+ subscription_id: Subscription ID
310
+ duration_days: Renewal duration in days
376
311
 
377
312
  Returns:
378
- SubscriptionResult with renewal status
313
+ SubscriptionResult: Renewal result
379
314
  """
380
315
  try:
381
- with transaction.atomic():
382
- subscription = Subscription.objects.filter(
383
- id=subscription_id
384
- ).first()
385
-
386
- if not subscription:
387
- return SubscriptionResult(
388
- success=False,
389
- error_code='SUBSCRIPTION_NOT_FOUND',
390
- error_message="Subscription not found"
391
- )
392
-
393
- # Calculate new expiry based on billing period
394
- now = timezone.now()
395
- if billing_period == 'yearly':
396
- new_expiry = now + timedelta(days=365)
397
- else: # Default to monthly
398
- new_expiry = now + timedelta(days=30)
399
-
400
- # Update subscription using correct enum
401
- subscription.expires_at = new_expiry
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
- expires_at=subscription.expires_at
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
- logger.error(f"Subscription renewal failed: {e}", exc_info=True)
416
- return SubscriptionResult(
417
- success=False,
418
- error_code='INTERNAL_ERROR',
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 get_subscription_analytics(
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
- Get subscription analytics for user.
371
+ Cancel subscription.
431
372
 
432
373
  Args:
433
- user: User object
434
- start_date: Analytics start date
435
- end_date: Analytics end date
374
+ subscription_id: Subscription ID
375
+ reason: Cancellation reason
436
376
 
437
377
  Returns:
438
- Analytics data dictionary
378
+ SubscriptionResult: Cancellation result
439
379
  """
440
380
  try:
441
- if not start_date:
442
- start_date = timezone.now() - timedelta(days=30)
443
- if not end_date:
444
- end_date = timezone.now()
445
-
446
- # Get subscriptions in date range
447
- subscriptions = Subscription.objects.filter(
448
- user=user,
449
- created_at__gte=start_date,
450
- created_at__lte=end_date
451
- ).select_related('endpoint_group')
452
-
453
- # Calculate analytics
454
- total_subscriptions = subscriptions.count()
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
- usage_by_endpoint = {}
461
- for sub in subscriptions:
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
- logger.error(f"Analytics calculation failed for user {user.id}: {e}")
496
- return {
497
- 'error': str(e),
498
- 'period': {
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 check_access(
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
- Check if user has access to endpoint group.
434
+ Get subscription statistics.
512
435
 
513
436
  Args:
514
- user_id: User ID
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
- Access check result
440
+ ServiceOperationResult: Subscription statistics
520
441
  """
521
442
  try:
522
- subscription = Subscription.objects.select_related('endpoint_group').get(
523
- user_id=user_id,
524
- endpoint_group__name=endpoint_group,
525
- status=Subscription.SubscriptionStatus.ACTIVE,
526
- expires_at__gt=timezone.now()
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
- # Check usage limit
530
- if subscription.usage_limit and subscription.usage_current >= subscription.usage_limit:
531
- return {
532
- 'has_access': False,
533
- 'reason': 'usage_limit_exceeded',
534
- 'usage_current': subscription.usage_current,
535
- 'usage_limit': subscription.usage_limit
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
- # Increment usage if requested
539
- if increment_usage:
540
- subscription.usage_current += 1
541
- subscription.save(update_fields=['usage_current'])
542
-
543
- return {
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
- except Subscription.DoesNotExist:
552
- return {
553
- 'has_access': False,
554
- 'reason': 'no_active_subscription'
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
- logger.error(f"Error checking access for user {user_id}, endpoint {endpoint_group}: {e}")
558
- return {
559
- 'has_access': False,
560
- 'reason': 'internal_error'
561
- }
494
+ return self._handle_exception("get_subscription_stats", e)
562
495
 
563
- def increment_usage(
564
- self,
565
- user_id: int,
566
- endpoint_group: str,
567
- amount: int = 1
568
- ) -> 'ServiceOperationResult':
569
- """
570
- Increment usage for user's subscription.
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
- Args:
573
- user_id: User ID
574
- endpoint_group: Endpoint group name
575
- amount: Amount to increment
576
-
577
- Returns:
578
- Usage increment result
579
- """
580
- try:
581
- subscription = Subscription.objects.select_related('endpoint_group').get(
582
- user_id=user_id,
583
- endpoint_group__name=endpoint_group,
584
- status=Subscription.SubscriptionStatus.ACTIVE,
585
- expires_at__gt=timezone.now()
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
- subscription.usage_current += amount
589
- subscription.save(update_fields=['usage_current'])
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 ServiceOperationResult(
593
- success=True,
594
- data={
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
- logger.error(f"Error incrementing usage for user {user_id}, endpoint {endpoint_group}: {e}")
608
- return ServiceOperationResult(
609
- success=False,
610
- error_message='internal_error'
611
- )
555
+ return self._handle_exception("health_check", e)