django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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 (125) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
  3. django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
  4. django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
  5. django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
  6. django_cfg/apps/payments/admin/__init__.py +23 -0
  7. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  8. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  9. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  10. django_cfg/apps/payments/admin/filters.py +259 -0
  11. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  12. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  13. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  14. django_cfg/apps/payments/config/__init__.py +65 -0
  15. django_cfg/apps/payments/config/module.py +70 -0
  16. django_cfg/apps/payments/config/providers.py +115 -0
  17. django_cfg/apps/payments/config/settings.py +96 -0
  18. django_cfg/apps/payments/config/utils.py +52 -0
  19. django_cfg/apps/payments/decorators.py +291 -0
  20. django_cfg/apps/payments/management/__init__.py +3 -0
  21. django_cfg/apps/payments/management/commands/README.md +178 -0
  22. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  23. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  24. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  25. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  26. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  27. django_cfg/apps/payments/middleware/api_access.py +294 -0
  28. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  29. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  30. django_cfg/apps/payments/migrations/0001_initial.py +125 -11
  31. django_cfg/apps/payments/models/__init__.py +18 -0
  32. django_cfg/apps/payments/models/api_keys.py +2 -2
  33. django_cfg/apps/payments/models/balance.py +2 -2
  34. django_cfg/apps/payments/models/base.py +16 -0
  35. django_cfg/apps/payments/models/events.py +2 -2
  36. django_cfg/apps/payments/models/payments.py +112 -2
  37. django_cfg/apps/payments/models/subscriptions.py +2 -2
  38. django_cfg/apps/payments/services/__init__.py +64 -7
  39. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  40. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  41. django_cfg/apps/payments/services/cache/base.py +30 -0
  42. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  43. django_cfg/apps/payments/services/core/__init__.py +17 -0
  44. django_cfg/apps/payments/services/core/balance_service.py +447 -0
  45. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  46. django_cfg/apps/payments/services/core/payment_service.py +576 -0
  47. django_cfg/apps/payments/services/core/subscription_service.py +614 -0
  48. django_cfg/apps/payments/services/internal_types.py +297 -0
  49. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  50. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  51. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  52. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  53. django_cfg/apps/payments/services/providers/__init__.py +22 -0
  54. django_cfg/apps/payments/services/providers/base.py +137 -0
  55. django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
  56. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  57. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  58. django_cfg/apps/payments/services/providers/registry.py +103 -0
  59. django_cfg/apps/payments/services/security/__init__.py +34 -0
  60. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  61. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  62. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  63. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  64. django_cfg/apps/payments/signals/__init__.py +13 -0
  65. django_cfg/apps/payments/signals/api_key_signals.py +160 -0
  66. django_cfg/apps/payments/signals/payment_signals.py +128 -0
  67. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  68. django_cfg/apps/payments/tasks/__init__.py +12 -0
  69. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  70. django_cfg/apps/payments/urls.py +5 -5
  71. django_cfg/apps/payments/utils/__init__.py +45 -0
  72. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  73. django_cfg/apps/payments/utils/config_utils.py +245 -0
  74. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  75. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  76. django_cfg/apps/payments/views/payment_views.py +40 -2
  77. django_cfg/apps/payments/views/webhook_views.py +266 -0
  78. django_cfg/apps/payments/viewsets.py +65 -0
  79. django_cfg/apps/support/signals.py +16 -4
  80. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  81. django_cfg/cli/README.md +2 -2
  82. django_cfg/cli/commands/create_project.py +1 -1
  83. django_cfg/cli/commands/info.py +1 -1
  84. django_cfg/cli/main.py +1 -1
  85. django_cfg/cli/utils.py +5 -5
  86. django_cfg/core/config.py +18 -4
  87. django_cfg/models/payments.py +546 -0
  88. django_cfg/models/revolution.py +1 -1
  89. django_cfg/models/tasks.py +51 -2
  90. django_cfg/modules/base.py +12 -6
  91. django_cfg/modules/django_currency/README.md +104 -269
  92. django_cfg/modules/django_currency/__init__.py +99 -41
  93. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  94. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  95. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  96. django_cfg/modules/django_currency/core/__init__.py +42 -0
  97. django_cfg/modules/django_currency/core/converter.py +169 -0
  98. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  99. django_cfg/modules/django_currency/core/models.py +54 -0
  100. django_cfg/modules/django_currency/database/__init__.py +25 -0
  101. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  102. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  103. django_cfg/modules/django_currency/utils/cache.py +92 -0
  104. django_cfg/modules/django_email.py +42 -4
  105. django_cfg/modules/django_unfold/dashboard.py +20 -0
  106. django_cfg/registry/core.py +10 -0
  107. django_cfg/template_archive/__init__.py +0 -0
  108. django_cfg/template_archive/django_sample.zip +0 -0
  109. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
  110. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
  111. django_cfg/apps/agents/examples/__init__.py +0 -3
  112. django_cfg/apps/agents/examples/simple_example.py +0 -161
  113. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  114. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  115. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  116. django_cfg/apps/payments/services/base.py +0 -68
  117. django_cfg/apps/payments/services/nowpayments.py +0 -78
  118. django_cfg/apps/payments/services/providers.py +0 -77
  119. django_cfg/apps/payments/services/redis_service.py +0 -215
  120. django_cfg/modules/django_currency/cache.py +0 -430
  121. django_cfg/modules/django_currency/converter.py +0 -324
  122. django_cfg/modules/django_currency/service.py +0 -277
  123. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  124. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  125. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,614 @@
1
+ """
2
+ Subscription Service - Core subscription management and access control.
3
+
4
+ This service handles subscription creation, renewal, access validation,
5
+ and usage tracking with Redis caching.
6
+ """
7
+
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
14
+ from django.utils import timezone
15
+ from pydantic import BaseModel, Field, ValidationError
16
+
17
+
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
+
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
+
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:
58
+ """
59
+ Universal subscription management service.
60
+
61
+ Handles subscription lifecycle, access control, and usage tracking
62
+ with support for multiple active subscriptions per user.
63
+ """
64
+
65
+ def __init__(self):
66
+ """Initialize subscription service with dependencies"""
67
+ pass
68
+
69
+ def create_subscription(self, subscription_data: dict) -> 'ServiceOperationResult':
70
+ """
71
+ Create new subscription for user.
72
+
73
+ Args:
74
+ subscription_data: Dictionary with subscription details
75
+
76
+ Returns:
77
+ ServiceOperationResult with subscription details
78
+ """
79
+ try:
80
+ # Get user
81
+ user = User.objects.get(id=subscription_data['user_id'])
82
+
83
+ # Get endpoint group
84
+ endpoint_group = EndpointGroup.objects.get(
85
+ name=subscription_data['endpoint_group_name'],
86
+ is_active=True
87
+ )
88
+
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()
97
+
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
+ )
103
+
104
+ # Create subscription
105
+ subscription = Subscription.objects.create(
106
+ user=user,
107
+ endpoint_group=endpoint_group,
108
+ tier=Subscription.SubscriptionTier.BASIC,
109
+ 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)
115
+ )
116
+
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
+ )
128
+
129
+ except Exception as e:
130
+ logger.error(f"Subscription creation failed: {e}", exc_info=True)
131
+
132
+ return ServiceOperationResult(
133
+ success=False,
134
+ error_message=f"Internal error: {str(e)}"
135
+ )
136
+
137
+ def check_endpoint_access(
138
+ self,
139
+ user: User,
140
+ endpoint_group_name: str,
141
+ use_cache: bool = True
142
+ ) -> AccessCheck:
143
+ """
144
+ Check if user has access to endpoint group.
145
+
146
+ Args:
147
+ user: User object
148
+ endpoint_group_name: Name of endpoint group
149
+ use_cache: Whether to use Redis cache
150
+
151
+ Returns:
152
+ AccessCheck with access status and details
153
+ """
154
+ 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()
169
+
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
190
+ )
191
+
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)
196
+
197
+ return result
198
+
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'
204
+ )
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
+
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
+ except Exception as e:
246
+ logger.error(f"Usage recording failed for user {user.id}: {e}")
247
+ return False
248
+
249
+ def get_user_subscriptions(
250
+ self,
251
+ user_id: int,
252
+ active_only: bool = True
253
+ ) -> List['SubscriptionInfo']:
254
+ """
255
+ Get user's subscriptions.
256
+
257
+ Args:
258
+ user_id: User ID
259
+ active_only: Return only active subscriptions
260
+
261
+ Returns:
262
+ List of subscription dictionaries
263
+ """
264
+ try:
265
+
266
+ # Query subscriptions
267
+ queryset = Subscription.objects.filter(user_id=user_id)
268
+
269
+ if active_only:
270
+ queryset = queryset.filter(
271
+ status=Subscription.SubscriptionStatus.ACTIVE,
272
+ expires_at__gt=timezone.now()
273
+ )
274
+
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
+ ]
303
+
304
+
305
+ return result
306
+
307
+ except Exception as e:
308
+ logger.error(f"Error getting subscriptions for user {user_id}: {e}")
309
+ return []
310
+
311
+ def cancel_subscription(
312
+ self,
313
+ user: User,
314
+ subscription_id: str,
315
+ reason: str = 'user_request'
316
+ ) -> SubscriptionResult:
317
+ """
318
+ Cancel user subscription.
319
+
320
+ Args:
321
+ user: User object
322
+ subscription_id: Subscription UUID
323
+ reason: Cancellation reason
324
+
325
+ Returns:
326
+ SubscriptionResult with cancellation status
327
+ """
328
+ 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)
358
+ )
359
+
360
+ 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)}"
366
+ )
367
+
368
+ def renew_subscription(
369
+ self,
370
+ subscription_id: str,
371
+ billing_period: Optional[str] = None
372
+ ) -> SubscriptionResult:
373
+ """
374
+ Renew expired or expiring subscription.
375
+
376
+ Args:
377
+ subscription_id: Subscription UUID
378
+ billing_period: New billing period (optional)
379
+
380
+ Returns:
381
+ SubscriptionResult with renewal status
382
+ """
383
+ 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()
409
+
410
+
411
+ return SubscriptionResult(
412
+ success=True,
413
+ subscription_id=str(subscription.id),
414
+ expires_at=subscription.expires_at
415
+ )
416
+
417
+ 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
+ )
424
+
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]:
432
+ """
433
+ Get subscription analytics for user.
434
+
435
+ Args:
436
+ user: User object
437
+ start_date: Analytics start date
438
+ end_date: Analytics end date
439
+
440
+ Returns:
441
+ Analytics data dictionary
442
+ """
443
+ 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()
462
+
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
+ }
496
+
497
+ 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
+ }
506
+
507
+ def check_access(
508
+ self,
509
+ user_id: int,
510
+ endpoint_group: str,
511
+ increment_usage: bool = False
512
+ ) -> Dict[str, Any]:
513
+ """
514
+ Check if user has access to endpoint group.
515
+
516
+ Args:
517
+ user_id: User ID
518
+ endpoint_group: Endpoint group name
519
+ increment_usage: Whether to increment usage count
520
+
521
+ Returns:
522
+ Access check result
523
+ """
524
+ 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()
530
+ )
531
+
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
+ }
540
+
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
552
+ }
553
+
554
+ except Subscription.DoesNotExist:
555
+ return {
556
+ 'has_access': False,
557
+ 'reason': 'no_active_subscription'
558
+ }
559
+ 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
+ }
565
+
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.
574
+
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()
589
+ )
590
+
591
+ subscription.usage_current += amount
592
+ subscription.save(update_fields=['usage_current'])
593
+
594
+
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
+ }
602
+ )
603
+
604
+ except Subscription.DoesNotExist:
605
+ return ServiceOperationResult(
606
+ success=False,
607
+ error_message='no_active_subscription'
608
+ )
609
+ 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
+ )