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
@@ -0,0 +1,641 @@
1
+ """
2
+ Subscription managers for the Universal Payment System v2.0.
3
+
4
+ Optimized querysets and managers for subscription and endpoint group operations.
5
+ """
6
+
7
+ from django.db import models
8
+ from django.utils import timezone
9
+ from datetime import timedelta
10
+ from django_cfg.modules.django_logger import get_logger
11
+
12
+ logger = get_logger("subscription_managers")
13
+
14
+
15
+ class SubscriptionQuerySet(models.QuerySet):
16
+ """
17
+ Optimized queryset for subscription operations.
18
+
19
+ Provides efficient queries for subscription management and access control.
20
+ """
21
+
22
+ def optimized(self):
23
+ """Prevent N+1 queries with select_related and prefetch_related."""
24
+ return self.select_related('user').prefetch_related('endpoint_groups')
25
+
26
+ def by_user(self, user):
27
+ """Filter subscriptions by user."""
28
+ return self.filter(user=user)
29
+
30
+ def by_tier(self, tier):
31
+ """Filter by subscription tier."""
32
+ return self.filter(tier=tier)
33
+
34
+ def by_status(self, status):
35
+ """Filter by subscription status."""
36
+ return self.filter(status=status)
37
+
38
+ # Status-based filters
39
+ def active(self):
40
+ """
41
+ Get active subscriptions that are not expired.
42
+
43
+ Returns subscriptions with status='active' and expires_at > now.
44
+ """
45
+ return self.filter(
46
+ status='active',
47
+ expires_at__gt=timezone.now()
48
+ )
49
+
50
+ def inactive(self):
51
+ """Get inactive subscriptions."""
52
+ return self.filter(status='inactive')
53
+
54
+ def suspended(self):
55
+ """Get suspended subscriptions."""
56
+ return self.filter(status='suspended')
57
+
58
+ def cancelled(self):
59
+ """Get cancelled subscriptions."""
60
+ return self.filter(status='cancelled')
61
+
62
+ def expired(self):
63
+ """
64
+ Get expired subscriptions.
65
+
66
+ Returns subscriptions where expires_at <= now, regardless of status.
67
+ """
68
+ return self.filter(expires_at__lte=timezone.now())
69
+
70
+ def expiring_soon(self, days=7):
71
+ """
72
+ Get subscriptions expiring in the next N days.
73
+
74
+ Args:
75
+ days: Number of days to look ahead (default: 7)
76
+ """
77
+ soon = timezone.now() + timedelta(days=days)
78
+ return self.filter(
79
+ expires_at__lte=soon,
80
+ expires_at__gt=timezone.now(),
81
+ status='active'
82
+ )
83
+
84
+ # Tier-based filters
85
+ def free_tier(self):
86
+ """Get free tier subscriptions."""
87
+ return self.filter(tier='free')
88
+
89
+ def basic_tier(self):
90
+ """Get basic tier subscriptions."""
91
+ return self.filter(tier='basic')
92
+
93
+ def pro_tier(self):
94
+ """Get pro tier subscriptions."""
95
+ return self.filter(tier='pro')
96
+
97
+ def enterprise_tier(self):
98
+ """Get enterprise tier subscriptions."""
99
+ return self.filter(tier='enterprise')
100
+
101
+ def paid_tiers(self):
102
+ """Get paid tier subscriptions (non-free)."""
103
+ return self.exclude(tier='free')
104
+
105
+ # Time-based filters
106
+ def created_recently(self, days=30):
107
+ """
108
+ Get subscriptions created in the last N days.
109
+
110
+ Args:
111
+ days: Number of days to look back (default: 30)
112
+ """
113
+ since = timezone.now() - timedelta(days=days)
114
+ return self.filter(created_at__gte=since)
115
+
116
+ def renewed_recently(self, days=30):
117
+ """
118
+ Get subscriptions renewed in the last N days.
119
+
120
+ Args:
121
+ days: Number of days to look back (default: 30)
122
+ """
123
+ since = timezone.now() - timedelta(days=days)
124
+ return self.filter(updated_at__gte=since, status='active')
125
+
126
+ # Usage-based filters
127
+ def with_usage(self):
128
+ """Get subscriptions that have been used (total_requests > 0)."""
129
+ return self.filter(total_requests__gt=0)
130
+
131
+ def without_usage(self):
132
+ """Get subscriptions that have never been used."""
133
+ return self.filter(total_requests=0)
134
+
135
+ def high_usage(self, threshold=1000):
136
+ """
137
+ Get subscriptions with high usage.
138
+
139
+ Args:
140
+ threshold: Request count threshold (default: 1000)
141
+ """
142
+ return self.filter(total_requests__gte=threshold)
143
+
144
+ def recent_usage(self, hours=24):
145
+ """
146
+ Get subscriptions used in the last N hours.
147
+
148
+ Args:
149
+ hours: Number of hours to look back (default: 24)
150
+ """
151
+ since = timezone.now() - timedelta(hours=hours)
152
+ return self.filter(last_request_at__gte=since)
153
+
154
+ # Auto-renewal filters
155
+ def auto_renewing(self):
156
+ """Get subscriptions with auto-renewal enabled."""
157
+ return self.filter(auto_renew=True)
158
+
159
+ def manual_renewal(self):
160
+ """Get subscriptions with manual renewal."""
161
+ return self.filter(auto_renew=False)
162
+
163
+ # Endpoint access filters
164
+ def with_endpoint_access(self, endpoint_group_code):
165
+ """
166
+ Get subscriptions with access to specific endpoint group.
167
+
168
+ Args:
169
+ endpoint_group_code: Endpoint group code to check
170
+ """
171
+ return self.filter(
172
+ endpoint_groups__code=endpoint_group_code,
173
+ endpoint_groups__is_enabled=True
174
+ )
175
+
176
+ # Aggregation methods
177
+ def total_revenue(self):
178
+ """Get total monthly revenue from active subscriptions."""
179
+ result = self.active().aggregate(total=models.Sum('monthly_cost_usd'))
180
+ return result['total'] or 0.0
181
+
182
+ def average_cost(self):
183
+ """Get average monthly cost."""
184
+ result = self.aggregate(avg=models.Avg('monthly_cost_usd'))
185
+ return result['avg'] or 0.0
186
+
187
+ def count_by_tier(self):
188
+ """Get count of subscriptions grouped by tier."""
189
+ return self.values('tier').annotate(count=models.Count('id')).order_by('tier')
190
+
191
+ def count_by_status(self):
192
+ """Get count of subscriptions grouped by status."""
193
+ return self.values('status').annotate(count=models.Count('id')).order_by('status')
194
+
195
+ def usage_stats(self):
196
+ """Get usage statistics."""
197
+ return self.aggregate(
198
+ total_requests=models.Sum('total_requests'),
199
+ avg_requests=models.Avg('total_requests'),
200
+ max_requests=models.Max('total_requests'),
201
+ active_users=models.Count('user', distinct=True)
202
+ )
203
+
204
+
205
+ class SubscriptionManager(models.Manager):
206
+ """
207
+ Manager for subscription operations with business logic.
208
+
209
+ Provides high-level methods for subscription management and access control.
210
+ """
211
+
212
+ def get_queryset(self):
213
+ """Return optimized queryset by default."""
214
+ return SubscriptionQuerySet(self.model, using=self._db)
215
+
216
+ def optimized(self):
217
+ """Get optimized queryset."""
218
+ return self.get_queryset().optimized()
219
+
220
+ # Status-based methods
221
+ def active(self):
222
+ """Get active subscriptions."""
223
+ return self.get_queryset().active()
224
+
225
+ def expired(self):
226
+ """Get expired subscriptions."""
227
+ return self.get_queryset().expired()
228
+
229
+ def expiring_soon(self, days=7):
230
+ """Get subscriptions expiring soon."""
231
+ return self.get_queryset().expiring_soon(days)
232
+
233
+ # Tier-based methods
234
+ def by_tier(self, tier):
235
+ """Get subscriptions by tier."""
236
+ return self.get_queryset().by_tier(tier)
237
+
238
+ def free_tier(self):
239
+ """Get free tier subscriptions."""
240
+ return self.get_queryset().free_tier()
241
+
242
+ def paid_tiers(self):
243
+ """Get paid tier subscriptions."""
244
+ return self.get_queryset().paid_tiers()
245
+
246
+ # User methods
247
+ def get_active_for_user(self, user):
248
+ """
249
+ Get active subscription for user.
250
+
251
+ Args:
252
+ user: User instance
253
+
254
+ Returns:
255
+ Subscription or None: Active subscription if exists
256
+ """
257
+ try:
258
+ return self.active().get(user=user)
259
+ except self.model.DoesNotExist:
260
+ return None
261
+
262
+ def has_active_subscription(self, user):
263
+ """
264
+ Check if user has an active subscription.
265
+
266
+ Args:
267
+ user: User instance
268
+
269
+ Returns:
270
+ bool: True if user has active subscription
271
+ """
272
+ return self.active().filter(user=user).exists()
273
+
274
+ def get_or_create_free_subscription(self, user):
275
+ """
276
+ Get existing subscription or create free tier subscription for user.
277
+
278
+ Args:
279
+ user: User instance
280
+
281
+ Returns:
282
+ tuple: (Subscription, created)
283
+ """
284
+ # Check for existing active subscription
285
+ existing = self.get_active_for_user(user)
286
+ if existing:
287
+ return existing, False
288
+
289
+ # Create free subscription
290
+ subscription = self.model.create_free_subscription(user)
291
+
292
+ logger.info(f"Created free subscription for user", extra={
293
+ 'user_id': user.id,
294
+ 'subscription_id': str(subscription.id)
295
+ })
296
+
297
+ return subscription, True
298
+
299
+ # Access control methods
300
+ def check_endpoint_access(self, user, endpoint_group_code):
301
+ """
302
+ Check if user has access to specific endpoint group.
303
+
304
+ Args:
305
+ user: User instance
306
+ endpoint_group_code: Endpoint group code to check
307
+
308
+ Returns:
309
+ bool: True if user has access
310
+ """
311
+ subscription = self.get_active_for_user(user)
312
+ if not subscription:
313
+ return False
314
+
315
+ return subscription.has_access_to_endpoint_group(endpoint_group_code)
316
+
317
+ def get_user_rate_limits(self, user):
318
+ """
319
+ Get rate limits for user based on their subscription.
320
+
321
+ Args:
322
+ user: User instance
323
+
324
+ Returns:
325
+ dict: Rate limit information
326
+ """
327
+ subscription = self.get_active_for_user(user)
328
+ if not subscription:
329
+ return {
330
+ 'requests_per_hour': 0,
331
+ 'requests_per_day': 0,
332
+ 'has_access': False
333
+ }
334
+
335
+ return {
336
+ 'requests_per_hour': subscription.requests_per_hour,
337
+ 'requests_per_day': subscription.requests_per_day,
338
+ 'has_access': True,
339
+ 'tier': subscription.tier,
340
+ 'expires_at': subscription.expires_at
341
+ }
342
+
343
+ # Maintenance methods
344
+ def cleanup_expired(self, dry_run=True):
345
+ """
346
+ Mark expired subscriptions as expired status.
347
+
348
+ Args:
349
+ dry_run: If True, only return count without making changes
350
+
351
+ Returns:
352
+ int: Number of subscriptions that would be/were updated
353
+ """
354
+ expired_subscriptions = self.filter(
355
+ expires_at__lte=timezone.now(),
356
+ status__in=['active', 'suspended']
357
+ )
358
+ count = expired_subscriptions.count()
359
+
360
+ if not dry_run and count > 0:
361
+ expired_subscriptions.update(status='expired')
362
+ logger.info(f"Marked {count} subscriptions as expired")
363
+
364
+ return count
365
+
366
+ def process_auto_renewals(self, dry_run=True):
367
+ """
368
+ Process auto-renewal for subscriptions expiring soon.
369
+
370
+ Args:
371
+ dry_run: If True, only return count without making changes
372
+
373
+ Returns:
374
+ int: Number of subscriptions that would be/were renewed
375
+ """
376
+ # Get subscriptions expiring in the next 24 hours with auto-renewal
377
+ expiring_subscriptions = self.filter(
378
+ expires_at__lte=timezone.now() + timedelta(hours=24),
379
+ expires_at__gt=timezone.now(),
380
+ auto_renew=True,
381
+ status='active'
382
+ )
383
+
384
+ count = expiring_subscriptions.count()
385
+
386
+ if not dry_run and count > 0:
387
+ for subscription in expiring_subscriptions:
388
+ try:
389
+ subscription.renew(duration_days=30)
390
+ logger.info(f"Auto-renewed subscription", extra={
391
+ 'subscription_id': str(subscription.id),
392
+ 'user_id': subscription.user.id
393
+ })
394
+ except Exception as e:
395
+ logger.error(f"Failed to auto-renew subscription: {e}", extra={
396
+ 'subscription_id': str(subscription.id),
397
+ 'user_id': subscription.user.id
398
+ })
399
+
400
+ return count
401
+
402
+ # Statistics methods
403
+ def get_subscription_stats(self, days=30):
404
+ """
405
+ Get subscription statistics for the last N days.
406
+
407
+ Args:
408
+ days: Number of days to analyze (default: 30)
409
+
410
+ Returns:
411
+ dict: Subscription statistics
412
+ """
413
+ queryset = self.get_queryset()
414
+ recent_queryset = queryset.created_recently(days)
415
+
416
+ stats = {
417
+ 'total_subscriptions': queryset.count(),
418
+ 'active_subscriptions': queryset.active().count(),
419
+ 'expired_subscriptions': queryset.expired().count(),
420
+ 'new_subscriptions': recent_queryset.count(),
421
+ 'total_revenue': queryset.total_revenue(),
422
+ 'average_cost': queryset.average_cost(),
423
+ 'by_tier': list(queryset.count_by_tier()),
424
+ 'by_status': list(queryset.count_by_status()),
425
+ 'usage_stats': queryset.usage_stats(),
426
+ 'auto_renewing': queryset.auto_renewing().count(),
427
+ 'expiring_soon': queryset.expiring_soon(7).count(),
428
+ }
429
+
430
+ logger.info(f"Generated subscription stats for {days} days", extra={
431
+ 'days': days,
432
+ 'total_subscriptions': stats['total_subscriptions'],
433
+ 'active_subscriptions': stats['active_subscriptions']
434
+ })
435
+
436
+ return stats
437
+
438
+ def get_tier_analytics(self):
439
+ """
440
+ Get detailed analytics by subscription tier.
441
+
442
+ Returns:
443
+ dict: Tier-based analytics
444
+ """
445
+ analytics = {}
446
+
447
+ for tier_code, tier_name in self.model.SubscriptionTier.choices:
448
+ tier_subscriptions = self.by_tier(tier_code)
449
+
450
+ analytics[tier_code] = {
451
+ 'name': tier_name,
452
+ 'total_count': tier_subscriptions.count(),
453
+ 'active_count': tier_subscriptions.active().count(),
454
+ 'revenue': tier_subscriptions.total_revenue(),
455
+ 'average_usage': tier_subscriptions.aggregate(
456
+ avg=models.Avg('total_requests')
457
+ )['avg'] or 0,
458
+ 'conversion_rate': 0.0 # Would need additional logic for conversion tracking
459
+ }
460
+
461
+ return analytics
462
+
463
+ # Business logic methods
464
+ def activate_subscription(self, subscription_id):
465
+ """
466
+ Activate subscription (business logic in manager).
467
+
468
+ Args:
469
+ subscription_id: Subscription ID or instance
470
+
471
+ Returns:
472
+ bool: True if subscription was activated successfully
473
+ """
474
+ try:
475
+ if isinstance(subscription_id, str):
476
+ subscription = self.get(id=subscription_id)
477
+ else:
478
+ subscription = subscription_id
479
+
480
+ subscription.status = subscription.model.SubscriptionStatus.ACTIVE
481
+ subscription.save(update_fields=['status', 'updated_at'])
482
+
483
+ logger.info(f"Subscription activated", extra={
484
+ 'subscription_id': str(subscription.id),
485
+ 'user_id': subscription.user.id
486
+ })
487
+
488
+ return True
489
+
490
+ except Exception as e:
491
+ logger.error(f"Failed to activate subscription: {e}", extra={
492
+ 'subscription_id': str(subscription_id) if hasattr(subscription_id, 'id') else subscription_id
493
+ })
494
+ return False
495
+
496
+ def suspend_subscription(self, subscription_id, reason=None):
497
+ """
498
+ Suspend subscription (business logic in manager).
499
+
500
+ Args:
501
+ subscription_id: Subscription ID or instance
502
+ reason: Suspension reason
503
+
504
+ Returns:
505
+ bool: True if subscription was suspended successfully
506
+ """
507
+ try:
508
+ if isinstance(subscription_id, str):
509
+ subscription = self.get(id=subscription_id)
510
+ else:
511
+ subscription = subscription_id
512
+
513
+ subscription.status = subscription.model.SubscriptionStatus.SUSPENDED
514
+ subscription.save(update_fields=['status', 'updated_at'])
515
+
516
+ logger.warning(f"Subscription suspended", extra={
517
+ 'subscription_id': str(subscription.id),
518
+ 'user_id': subscription.user.id,
519
+ 'reason': reason
520
+ })
521
+
522
+ return True
523
+
524
+ except Exception as e:
525
+ logger.error(f"Failed to suspend subscription: {e}", extra={
526
+ 'subscription_id': str(subscription_id) if hasattr(subscription_id, 'id') else subscription_id
527
+ })
528
+ return False
529
+
530
+ def cancel_subscription(self, subscription_id, reason=None):
531
+ """
532
+ Cancel subscription (business logic in manager).
533
+
534
+ Args:
535
+ subscription_id: Subscription ID or instance
536
+ reason: Cancellation reason
537
+
538
+ Returns:
539
+ bool: True if subscription was cancelled successfully
540
+ """
541
+ try:
542
+ if isinstance(subscription_id, str):
543
+ subscription = self.get(id=subscription_id)
544
+ else:
545
+ subscription = subscription_id
546
+
547
+ subscription.status = subscription.model.SubscriptionStatus.CANCELLED
548
+ subscription.save(update_fields=['status', 'updated_at'])
549
+
550
+ logger.info(f"Subscription cancelled", extra={
551
+ 'subscription_id': str(subscription.id),
552
+ 'user_id': subscription.user.id,
553
+ 'reason': reason
554
+ })
555
+
556
+ return True
557
+
558
+ except Exception as e:
559
+ logger.error(f"Failed to cancel subscription: {e}", extra={
560
+ 'subscription_id': str(subscription_id) if hasattr(subscription_id, 'id') else subscription_id
561
+ })
562
+ return False
563
+
564
+ def renew_subscription(self, subscription_id, duration_days=30):
565
+ """
566
+ Renew subscription (business logic in manager).
567
+
568
+ Args:
569
+ subscription_id: Subscription ID or instance
570
+ duration_days: Duration in days to extend
571
+
572
+ Returns:
573
+ bool: True if subscription was renewed successfully
574
+ """
575
+ try:
576
+ if isinstance(subscription_id, str):
577
+ subscription = self.get(id=subscription_id)
578
+ else:
579
+ subscription = subscription_id
580
+
581
+ from datetime import timedelta
582
+
583
+ if subscription.is_expired:
584
+ # If expired, start from now
585
+ subscription.starts_at = timezone.now()
586
+ subscription.expires_at = subscription.starts_at + timedelta(days=duration_days)
587
+ else:
588
+ # If not expired, extend from current expiration
589
+ subscription.expires_at += timedelta(days=duration_days)
590
+
591
+ subscription.status = subscription.model.SubscriptionStatus.ACTIVE
592
+ subscription.save(update_fields=['starts_at', 'expires_at', 'status', 'updated_at'])
593
+
594
+ logger.info(f"Subscription renewed", extra={
595
+ 'subscription_id': str(subscription.id),
596
+ 'user_id': subscription.user.id,
597
+ 'duration_days': duration_days,
598
+ 'new_expires_at': subscription.expires_at.isoformat()
599
+ })
600
+
601
+ return True
602
+
603
+ except Exception as e:
604
+ logger.error(f"Failed to renew subscription: {e}", extra={
605
+ 'subscription_id': str(subscription_id) if hasattr(subscription_id, 'id') else subscription_id
606
+ })
607
+ return False
608
+
609
+ def increment_subscription_usage(self, subscription_id):
610
+ """
611
+ Increment usage counter for subscription (business logic in manager).
612
+
613
+ Args:
614
+ subscription_id: Subscription ID or instance
615
+
616
+ Returns:
617
+ bool: True if usage was incremented successfully
618
+ """
619
+ try:
620
+ if isinstance(subscription_id, str):
621
+ subscription = self.get(id=subscription_id)
622
+ else:
623
+ subscription = subscription_id
624
+
625
+ subscription.total_requests += 1
626
+ subscription.last_request_at = timezone.now()
627
+ subscription.save(update_fields=['total_requests', 'last_request_at', 'updated_at'])
628
+
629
+ logger.debug(f"Incremented subscription usage", extra={
630
+ 'subscription_id': str(subscription.id),
631
+ 'user_id': subscription.user.id,
632
+ 'total_requests': subscription.total_requests
633
+ })
634
+
635
+ return True
636
+
637
+ except Exception as e:
638
+ logger.error(f"Failed to increment subscription usage: {e}", extra={
639
+ 'subscription_id': str(subscription_id) if hasattr(subscription_id, 'id') else subscription_id
640
+ })
641
+ return False