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.
Files changed (258) 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 -9
  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 +600 -108
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +470 -64
  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 +381 -0
  39. django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
  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 +343 -163
  43. django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +16 -20
  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 +207 -67
  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 -284
  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 +344 -468
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -484
  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 +232 -71
  74. django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
  75. django_cfg/apps/payments/services/providers/registry.py +429 -80
  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 +211 -130
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +129 -98
  85. django_cfg/apps/payments/signals/subscription_signals.py +195 -143
  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 +46 -47
  93. django_cfg/apps/payments/urls_admin.py +49 -0
  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/apps/tasks/urls.py +0 -2
  110. django_cfg/apps/tasks/urls_admin.py +14 -0
  111. django_cfg/apps/urls.py +4 -4
  112. django_cfg/config.py +1 -1
  113. django_cfg/core/config.py +75 -4
  114. django_cfg/core/generation.py +25 -4
  115. django_cfg/core/integration/README.md +363 -0
  116. django_cfg/core/integration/__init__.py +47 -0
  117. django_cfg/core/integration/commands_collector.py +239 -0
  118. django_cfg/core/integration/display/__init__.py +15 -0
  119. django_cfg/core/integration/display/base.py +157 -0
  120. django_cfg/core/integration/display/ngrok.py +164 -0
  121. django_cfg/core/integration/display/startup.py +815 -0
  122. django_cfg/core/integration/url_integration.py +123 -0
  123. django_cfg/core/integration/version_checker.py +160 -0
  124. django_cfg/management/commands/auto_generate.py +4 -0
  125. django_cfg/management/commands/check_settings.py +6 -0
  126. django_cfg/management/commands/clear_constance.py +5 -2
  127. django_cfg/management/commands/create_token.py +6 -0
  128. django_cfg/management/commands/list_urls.py +6 -0
  129. django_cfg/management/commands/migrate_all.py +6 -0
  130. django_cfg/management/commands/migrator.py +3 -0
  131. django_cfg/management/commands/rundramatiq.py +6 -0
  132. django_cfg/management/commands/runserver_ngrok.py +51 -29
  133. django_cfg/management/commands/script.py +6 -0
  134. django_cfg/management/commands/show_config.py +12 -2
  135. django_cfg/management/commands/show_urls.py +4 -0
  136. django_cfg/management/commands/superuser.py +6 -0
  137. django_cfg/management/commands/task_clear.py +4 -1
  138. django_cfg/management/commands/task_status.py +3 -1
  139. django_cfg/management/commands/test_email.py +3 -0
  140. django_cfg/management/commands/test_telegram.py +6 -0
  141. django_cfg/management/commands/test_twilio.py +6 -0
  142. django_cfg/management/commands/tree.py +6 -0
  143. django_cfg/management/commands/validate_config.py +155 -149
  144. django_cfg/models/constance.py +31 -11
  145. django_cfg/models/payments.py +175 -498
  146. django_cfg/modules/django_currency/__init__.py +16 -11
  147. django_cfg/modules/django_currency/clients/__init__.py +4 -4
  148. django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
  149. django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
  150. django_cfg/modules/django_currency/core/__init__.py +1 -7
  151. django_cfg/modules/django_currency/core/converter.py +18 -23
  152. django_cfg/modules/django_currency/core/models.py +122 -11
  153. django_cfg/modules/django_currency/database/__init__.py +4 -4
  154. django_cfg/modules/django_currency/database/database_loader.py +190 -309
  155. django_cfg/modules/django_logger.py +160 -146
  156. django_cfg/modules/django_unfold/dashboard.py +65 -12
  157. django_cfg/registry/core.py +1 -0
  158. django_cfg/template_archive/django_sample.zip +0 -0
  159. django_cfg/templates/admin/components/action_grid.html +9 -9
  160. django_cfg/templates/admin/components/metric_card.html +5 -5
  161. django_cfg/templates/admin/components/status_badge.html +2 -2
  162. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
  163. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
  164. django_cfg/templates/admin/snippets/components/system_health.html +1 -1
  165. django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
  166. django_cfg/utils/smart_defaults.py +222 -571
  167. django_cfg/utils/toolkit.py +51 -11
  168. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
  169. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
  170. django_cfg/apps/payments/__init__.py +0 -8
  171. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  172. django_cfg/apps/payments/config/module.py +0 -70
  173. django_cfg/apps/payments/config/providers.py +0 -105
  174. django_cfg/apps/payments/config/settings.py +0 -96
  175. django_cfg/apps/payments/config/utils.py +0 -52
  176. django_cfg/apps/payments/decorators.py +0 -291
  177. django_cfg/apps/payments/management/commands/README.md +0 -178
  178. django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
  179. django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
  180. django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
  181. django_cfg/apps/payments/managers/__init__.py +0 -22
  182. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  183. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  184. django_cfg/apps/payments/managers/currency_manager.py +0 -83
  185. django_cfg/apps/payments/managers/payment_manager.py +0 -44
  186. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  187. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  188. django_cfg/apps/payments/models/events.py +0 -73
  189. django_cfg/apps/payments/serializers/__init__.py +0 -56
  190. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  191. django_cfg/apps/payments/serializers/balance.py +0 -59
  192. django_cfg/apps/payments/serializers/currencies.py +0 -55
  193. django_cfg/apps/payments/serializers/payments.py +0 -62
  194. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  195. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  196. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  197. django_cfg/apps/payments/services/cache/base.py +0 -30
  198. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  199. django_cfg/apps/payments/services/internal_types.py +0 -297
  200. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  201. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  202. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -222
  203. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  204. django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
  205. django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
  206. django_cfg/apps/payments/services/security/__init__.py +0 -34
  207. django_cfg/apps/payments/services/security/error_handler.py +0 -637
  208. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  209. django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
  210. django_cfg/apps/payments/services/validators/__init__.py +0 -8
  211. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  212. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  213. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  214. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  215. django_cfg/apps/payments/tasks/__init__.py +0 -12
  216. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  217. django_cfg/apps/payments/templates/payments/base.html +0 -182
  218. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  219. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  220. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -36
  221. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  222. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
  223. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
  224. django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
  225. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  226. django_cfg/apps/payments/urls_templates.py +0 -52
  227. django_cfg/apps/payments/utils/__init__.py +0 -45
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -245
  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 -62
  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 -111
  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 -312
  241. django_cfg/apps/payments/views/templates/base.py +0 -204
  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 -164
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -240
  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 -65
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
  252. django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
  253. django_cfg/template_archive/.gitignore +0 -1
  254. django_cfg/template_archive/__init__.py +0 -0
  255. django_cfg/urls.py +0 -33
  256. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  257. {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  258. {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 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
- import logging
9
- from typing import Dict, Any, Optional, List
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
-
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
- 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
- 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
- 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
+ )
306
250
 
307
251
  except Exception as e:
308
- logger.error(f"Error getting subscriptions for user {user_id}: {e}")
309
- return []
252
+ return self._handle_exception(
253
+ "check_access", e,
254
+ user_id=user_id,
255
+ endpoint_group=endpoint_group
256
+ )
310
257
 
311
- def cancel_subscription(
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
- Cancel user subscription.
260
+ Increment subscription usage counter.
319
261
 
320
262
  Args:
321
- user: User object
322
- subscription_id: Subscription UUID
323
- reason: Cancellation reason
263
+ user_id: User ID
324
264
 
325
265
  Returns:
326
- SubscriptionResult with cancellation status
266
+ ServiceOperationResult: Usage increment result
327
267
  """
328
268
  try:
329
- with transaction.atomic():
330
- subscription = Subscription.objects.filter(
331
- id=subscription_id,
332
- user=user,
333
- status=Subscription.Status.ACTIVE
334
- ).first()
335
-
336
- if not subscription:
337
- return SubscriptionResult(
338
- success=False,
339
- error_code='SUBSCRIPTION_NOT_FOUND',
340
- error_message="Active subscription not found"
341
- )
342
-
343
- # Cancel subscription
344
- subscription.status = Subscription.Status.CANCELLED
345
- subscription.auto_renew = False
346
- subscription.next_billing_at = None
347
- subscription.metadata = {
348
- **subscription.metadata,
349
- 'cancellation_reason': reason,
350
- 'cancelled_at': timezone.now().isoformat()
351
- }
352
- subscription.save()
353
-
354
-
355
- return SubscriptionResult(
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
- logger.error(f"Subscription cancellation failed: {e}", exc_info=True)
362
- return SubscriptionResult(
363
- success=False,
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 expired or expiring subscription.
306
+ Renew existing subscription.
375
307
 
376
308
  Args:
377
- subscription_id: Subscription UUID
378
- billing_period: New billing period (optional)
309
+ subscription_id: Subscription ID
310
+ duration_days: Renewal duration in days
379
311
 
380
312
  Returns:
381
- SubscriptionResult with renewal status
313
+ SubscriptionResult: Renewal result
382
314
  """
383
315
  try:
384
- with transaction.atomic():
385
- subscription = Subscription.objects.filter(
386
- id=subscription_id
387
- ).first()
388
-
389
- if not subscription:
390
- return SubscriptionResult(
391
- success=False,
392
- error_code='SUBSCRIPTION_NOT_FOUND',
393
- error_message="Subscription not found"
394
- )
395
-
396
- # Calculate new expiry based on billing period
397
- now = timezone.now()
398
- if billing_period == 'yearly':
399
- new_expiry = now + timedelta(days=365)
400
- else: # Default to monthly
401
- new_expiry = now + timedelta(days=30)
402
-
403
- # Update subscription using correct enum
404
- subscription.expires_at = new_expiry
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
- 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"
415
361
  )
416
362
 
417
363
  except Exception as e:
418
- logger.error(f"Subscription renewal failed: {e}", exc_info=True)
419
- return SubscriptionResult(
420
- success=False,
421
- error_code='INTERNAL_ERROR',
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
- Get subscription analytics for user.
371
+ Cancel subscription.
434
372
 
435
373
  Args:
436
- user: User object
437
- start_date: Analytics start date
438
- end_date: Analytics end date
374
+ subscription_id: Subscription ID
375
+ reason: Cancellation reason
439
376
 
440
377
  Returns:
441
- Analytics data dictionary
378
+ SubscriptionResult: Cancellation result
442
379
  """
443
380
  try:
444
- if not start_date:
445
- start_date = timezone.now() - timedelta(days=30)
446
- if not end_date:
447
- end_date = timezone.now()
448
-
449
- # Get subscriptions in date range
450
- subscriptions = Subscription.objects.filter(
451
- user=user,
452
- created_at__gte=start_date,
453
- created_at__lte=end_date
454
- ).select_related('endpoint_group')
455
-
456
- # Calculate analytics
457
- total_subscriptions = subscriptions.count()
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
- usage_by_endpoint = {}
464
- for sub in subscriptions:
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
- logger.error(f"Analytics calculation failed for user {user.id}: {e}")
499
- return {
500
- 'error': str(e),
501
- 'period': {
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 check_access(
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
- Check if user has access to endpoint group.
434
+ Get subscription statistics.
515
435
 
516
436
  Args:
517
- user_id: User ID
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
- Access check result
440
+ ServiceOperationResult: Subscription statistics
523
441
  """
524
442
  try:
525
- subscription = Subscription.objects.select_related('endpoint_group').get(
526
- user_id=user_id,
527
- endpoint_group__name=endpoint_group,
528
- status=Subscription.SubscriptionStatus.ACTIVE,
529
- 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
+ )
530
460
  )
531
461
 
532
- # Check usage limit
533
- if subscription.usage_limit and subscription.usage_current >= subscription.usage_limit:
534
- return {
535
- 'has_access': False,
536
- 'reason': 'usage_limit_exceeded',
537
- 'usage_current': subscription.usage_current,
538
- 'usage_limit': subscription.usage_limit
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
- # Increment usage if requested
542
- if increment_usage:
543
- subscription.usage_current += 1
544
- subscription.save(update_fields=['usage_current'])
545
-
546
- return {
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
- except Subscription.DoesNotExist:
555
- return {
556
- 'has_access': False,
557
- 'reason': 'no_active_subscription'
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
- logger.error(f"Error checking access for user {user_id}, endpoint {endpoint_group}: {e}")
561
- return {
562
- 'has_access': False,
563
- 'reason': 'internal_error'
564
- }
494
+ return self._handle_exception("get_subscription_stats", e)
565
495
 
566
- def increment_usage(
567
- self,
568
- user_id: int,
569
- endpoint_group: str,
570
- amount: int = 1
571
- ) -> 'ServiceOperationResult':
572
- """
573
- 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
+ }
574
504
 
575
- Args:
576
- user_id: User ID
577
- endpoint_group: Endpoint group name
578
- amount: Amount to increment
579
-
580
- Returns:
581
- Usage increment result
582
- """
583
- try:
584
- subscription = Subscription.objects.select_related('endpoint_group').get(
585
- user_id=user_id,
586
- endpoint_group__name=endpoint_group,
587
- status=Subscription.SubscriptionStatus.ACTIVE,
588
- 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"
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
- subscription.usage_current += amount
592
- 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()
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 ServiceOperationResult(
596
- success=True,
597
- data={
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
- logger.error(f"Error incrementing usage for user {user_id}, endpoint {endpoint_group}: {e}")
611
- return ServiceOperationResult(
612
- success=False,
613
- error_message='internal_error'
614
- )
555
+ return self._handle_exception("health_check", e)