django-cfg 1.2.22__py3-none-any.whl → 1.2.23__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 (67) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin/__init__.py +23 -0
  3. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  4. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  5. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  6. django_cfg/apps/payments/admin/filters.py +259 -0
  7. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  8. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  9. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  10. django_cfg/apps/payments/config/__init__.py +87 -0
  11. django_cfg/apps/payments/config/module.py +162 -0
  12. django_cfg/apps/payments/config/providers.py +93 -0
  13. django_cfg/apps/payments/config/settings.py +136 -0
  14. django_cfg/apps/payments/config/utils.py +198 -0
  15. django_cfg/apps/payments/decorators.py +291 -0
  16. django_cfg/apps/payments/middleware/api_access.py +261 -0
  17. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  18. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  19. django_cfg/apps/payments/migrations/0001_initial.py +32 -11
  20. django_cfg/apps/payments/models/__init__.py +18 -0
  21. django_cfg/apps/payments/models/api_keys.py +2 -2
  22. django_cfg/apps/payments/models/balance.py +2 -2
  23. django_cfg/apps/payments/models/base.py +16 -0
  24. django_cfg/apps/payments/models/events.py +2 -2
  25. django_cfg/apps/payments/models/payments.py +2 -2
  26. django_cfg/apps/payments/models/subscriptions.py +2 -2
  27. django_cfg/apps/payments/services/__init__.py +58 -7
  28. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  29. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  30. django_cfg/apps/payments/services/cache/base.py +30 -0
  31. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  32. django_cfg/apps/payments/services/core/__init__.py +17 -0
  33. django_cfg/apps/payments/services/core/balance_service.py +449 -0
  34. django_cfg/apps/payments/services/core/payment_service.py +393 -0
  35. django_cfg/apps/payments/services/core/subscription_service.py +616 -0
  36. django_cfg/apps/payments/services/internal_types.py +266 -0
  37. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  38. django_cfg/apps/payments/services/providers/__init__.py +19 -0
  39. django_cfg/apps/payments/services/providers/base.py +137 -0
  40. django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
  41. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  42. django_cfg/apps/payments/services/providers/registry.py +99 -0
  43. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  44. django_cfg/apps/payments/signals/__init__.py +13 -0
  45. django_cfg/apps/payments/signals/api_key_signals.py +150 -0
  46. django_cfg/apps/payments/signals/payment_signals.py +127 -0
  47. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  48. django_cfg/apps/payments/urls.py +5 -5
  49. django_cfg/apps/payments/utils/__init__.py +42 -0
  50. django_cfg/apps/payments/utils/config_utils.py +243 -0
  51. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  52. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  53. django_cfg/apps/support/signals.py +16 -4
  54. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  55. django_cfg/models/revolution.py +1 -1
  56. django_cfg/modules/base.py +1 -1
  57. django_cfg/modules/django_email.py +42 -4
  58. django_cfg/modules/django_unfold/dashboard.py +20 -0
  59. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
  60. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/RECORD +63 -26
  61. django_cfg/apps/payments/services/base.py +0 -68
  62. django_cfg/apps/payments/services/nowpayments.py +0 -78
  63. django_cfg/apps/payments/services/providers.py +0 -77
  64. django_cfg/apps/payments/services/redis_service.py +0 -215
  65. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
  66. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
  67. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,196 @@
1
+ """
2
+ 🔄 Universal Subscription Signals
3
+
4
+ Automatic subscription management and lifecycle handling via Django signals.
5
+ """
6
+
7
+ from django.db.models.signals import post_save, pre_save, post_delete
8
+ from django.dispatch import receiver
9
+ from django.db import transaction
10
+ from django.utils import timezone
11
+ from datetime import timedelta
12
+ import logging
13
+
14
+ from ..models import Subscription, EndpointGroup, UserBalance, Transaction
15
+ from ..services.cache import SimpleCache
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @receiver(pre_save, sender=Subscription)
21
+ def store_original_subscription_status(sender, instance, **kwargs):
22
+ """Store original subscription status for change detection."""
23
+ if instance.pk:
24
+ try:
25
+ old_instance = Subscription.objects.get(pk=instance.pk)
26
+ instance._original_status = old_instance.status
27
+ instance._original_expires_at = old_instance.expires_at
28
+ except Subscription.DoesNotExist:
29
+ instance._original_status = None
30
+ instance._original_expires_at = None
31
+
32
+
33
+ @receiver(post_save, sender=Subscription)
34
+ def process_subscription_status_changes(sender, instance, created, **kwargs):
35
+ """Process subscription status changes and handle lifecycle events."""
36
+ if created:
37
+ logger.info(
38
+ f"New subscription created: {instance.endpoint_group.name} "
39
+ f"for user {instance.user.email} (expires: {instance.expires_at})"
40
+ )
41
+ _clear_user_cache(instance.user.id)
42
+ return
43
+
44
+ # Check if status changed
45
+ if hasattr(instance, '_original_status'):
46
+ old_status = instance._original_status
47
+ new_status = instance.status
48
+
49
+ if old_status != new_status:
50
+ logger.info(
51
+ f"Subscription status changed: {instance.endpoint_group.name} "
52
+ f"for user {instance.user.email} - {old_status} → {new_status}"
53
+ )
54
+
55
+ # Handle specific status changes
56
+ if new_status == Subscription.SubscriptionStatus.ACTIVE:
57
+ _handle_subscription_activation(instance)
58
+ elif new_status == Subscription.SubscriptionStatus.CANCELLED:
59
+ _handle_subscription_cancellation(instance)
60
+ elif new_status == Subscription.SubscriptionStatus.EXPIRED:
61
+ _handle_subscription_expiration(instance)
62
+
63
+ _clear_user_cache(instance.user.id)
64
+
65
+
66
+ @receiver(post_save, sender=Subscription)
67
+ def handle_subscription_renewal(sender, instance, created, **kwargs):
68
+ """Handle subscription renewal and billing."""
69
+ if created or not hasattr(instance, '_original_expires_at'):
70
+ return
71
+
72
+ old_expires_at = instance._original_expires_at
73
+ new_expires_at = instance.expires_at
74
+
75
+ # Check if subscription was renewed (expires_at extended)
76
+ if old_expires_at and new_expires_at and new_expires_at > old_expires_at:
77
+ logger.info(
78
+ f"Subscription renewed: {instance.endpoint_group.name} "
79
+ f"for user {instance.user.email} - extended to {new_expires_at}"
80
+ )
81
+ _clear_user_cache(instance.user.id)
82
+
83
+
84
+ @receiver(post_delete, sender=Subscription)
85
+ def log_subscription_deletion(sender, instance, **kwargs):
86
+ """Log subscription deletions for audit purposes."""
87
+ logger.warning(
88
+ f"Subscription deleted: {instance.endpoint_group.name} "
89
+ f"for user {instance.user.email} - Status was: {instance.status}"
90
+ )
91
+ _clear_user_cache(instance.user.id)
92
+
93
+
94
+ @receiver(post_save, sender=EndpointGroup)
95
+ def log_endpoint_group_changes(sender, instance, created, **kwargs):
96
+ """Log endpoint group changes that may affect subscriptions."""
97
+ if created:
98
+ logger.info(f"New endpoint group created: {instance.name}")
99
+ else:
100
+ # Check if important fields changed
101
+ if instance.tracker.has_changed('is_active'):
102
+ logger.warning(
103
+ f"Endpoint group activity changed: {instance.name} "
104
+ f"- active: {instance.is_active}"
105
+ )
106
+ # Clear cache for all users with subscriptions to this group
107
+ _clear_endpoint_group_cache(instance)
108
+
109
+
110
+ def _handle_subscription_activation(subscription: Subscription):
111
+ """Handle subscription activation logic."""
112
+ try:
113
+ # Reset usage counters
114
+ subscription.usage_current = 0
115
+
116
+ # Set next billing date
117
+ if not subscription.next_billing:
118
+ subscription.next_billing = timezone.now() + timedelta(days=30) # Monthly by default
119
+
120
+ subscription.save(update_fields=['usage_current', 'next_billing'])
121
+
122
+ logger.info(f"Subscription activated: {subscription.endpoint_group.name} for {subscription.user.email}")
123
+
124
+ except Exception as e:
125
+ logger.error(f"Error handling subscription activation: {e}")
126
+
127
+
128
+ def _handle_subscription_cancellation(subscription: Subscription):
129
+ """Handle subscription cancellation logic."""
130
+ try:
131
+ # Mark as cancelled
132
+ subscription.cancelled_at = timezone.now()
133
+ subscription.save(update_fields=['cancelled_at'])
134
+
135
+ logger.info(f"Subscription cancelled: {subscription.endpoint_group.name} for {subscription.user.email}")
136
+
137
+ except Exception as e:
138
+ logger.error(f"Error handling subscription cancellation: {e}")
139
+
140
+
141
+ def _handle_subscription_expiration(subscription: Subscription):
142
+ """Handle subscription expiration logic."""
143
+ try:
144
+ # Mark as expired
145
+ subscription.expired_at = timezone.now()
146
+ subscription.save(update_fields=['expired_at'])
147
+
148
+ logger.info(f"Subscription expired: {subscription.endpoint_group.name} for {subscription.user.email}")
149
+
150
+ except Exception as e:
151
+ logger.error(f"Error handling subscription expiration: {e}")
152
+
153
+
154
+ def _clear_user_cache(user_id: int):
155
+ """Clear cache for specific user."""
156
+ try:
157
+ cache = SimpleCache("subscriptions")
158
+ cache_keys = [
159
+ f"access:{user_id}",
160
+ f"subscriptions:{user_id}",
161
+ f"user_summary:{user_id}",
162
+ ]
163
+
164
+ for key in cache_keys:
165
+ cache.delete(key)
166
+
167
+ except Exception as e:
168
+ logger.warning(f"Failed to clear cache for user {user_id}: {e}")
169
+
170
+
171
+ def _clear_endpoint_group_cache(endpoint_group: EndpointGroup):
172
+ """Clear cache for all users with subscriptions to this endpoint group."""
173
+ try:
174
+ # Get all users with active subscriptions to this group
175
+ user_ids = Subscription.objects.filter(
176
+ endpoint_group=endpoint_group,
177
+ status=Subscription.SubscriptionStatus.ACTIVE
178
+ ).values_list('user_id', flat=True)
179
+
180
+ for user_id in user_ids:
181
+ _clear_user_cache(user_id)
182
+
183
+ except Exception as e:
184
+ logger.warning(f"Failed to clear cache for endpoint group {endpoint_group.name}: {e}")
185
+
186
+
187
+ @receiver(post_save, sender=Subscription)
188
+ def update_usage_statistics(sender, instance, created, **kwargs):
189
+ """Update usage statistics when subscription is modified."""
190
+ if not created and hasattr(instance, '_original_status'):
191
+ # Only update stats if usage-related fields might have changed
192
+ if instance.usage_current != getattr(instance, '_original_usage_current', instance.usage_current):
193
+ logger.debug(
194
+ f"Usage updated for subscription {instance.endpoint_group.name}: "
195
+ f"{instance.usage_current} requests"
196
+ )
@@ -66,13 +66,13 @@ generic_patterns = [
66
66
 
67
67
  urlpatterns = [
68
68
  # Include all router URLs
69
- path('api/v1/', include(router.urls)),
69
+ path('', include(router.urls)),
70
70
 
71
71
  # Include nested router URLs
72
- path('api/v1/', include(payments_router.urls)),
73
- path('api/v1/', include(subscriptions_router.urls)),
74
- path('api/v1/', include(apikeys_router.urls)),
72
+ path('', include(payments_router.urls)),
73
+ path('', include(subscriptions_router.urls)),
74
+ path('', include(apikeys_router.urls)),
75
75
 
76
76
  # Include generic API endpoints
77
- path('api/v1/', include(generic_patterns)),
77
+ path('', include(generic_patterns)),
78
78
  ]
@@ -0,0 +1,42 @@
1
+ """
2
+ Utilities for universal payments.
3
+ """
4
+
5
+ from .middleware_utils import get_client_ip, is_api_request, extract_api_key
6
+ # from .billing_utils import calculate_usage_cost, create_billing_transaction # TODO: Implement when needed
7
+ from .validation_utils import validate_api_key, check_subscription_access
8
+
9
+ # Configuration utilities
10
+ from .config_utils import (
11
+ PaymentsConfigUtil,
12
+ RedisConfigHelper,
13
+ CacheConfigHelper,
14
+ ProviderConfigHelper,
15
+ get_payments_config,
16
+ is_payments_enabled,
17
+ is_debug_mode
18
+ )
19
+
20
+ __all__ = [
21
+ # Middleware utilities
22
+ 'get_client_ip',
23
+ 'is_api_request',
24
+ 'extract_api_key',
25
+
26
+ # Billing utilities (TODO: Implement when needed)
27
+ # 'calculate_usage_cost',
28
+ # 'create_billing_transaction',
29
+
30
+ # Validation utilities
31
+ 'validate_api_key',
32
+ 'check_subscription_access',
33
+
34
+ # Configuration utilities
35
+ 'PaymentsConfigUtil',
36
+ 'RedisConfigHelper',
37
+ 'CacheConfigHelper',
38
+ 'ProviderConfigHelper',
39
+ 'get_payments_config',
40
+ 'is_payments_enabled',
41
+ 'is_debug_mode',
42
+ ]
@@ -0,0 +1,243 @@
1
+ """
2
+ Configuration utilities for payments module.
3
+
4
+ Universal utilities for working with django-cfg settings and configuration.
5
+ """
6
+
7
+ import logging
8
+ from typing import Optional, Dict, Any, Type
9
+ from django.conf import settings
10
+
11
+ from django_cfg.modules.base import BaseCfgModule
12
+ from ..config.settings import PaymentsSettings
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class PaymentsConfigMixin:
18
+ """Mixin for accessing payments configuration through django-cfg."""
19
+
20
+ _payments_config_cache: Optional[PaymentsSettings] = None
21
+ _config_module: Optional[BaseCfgModule] = None
22
+
23
+ @classmethod
24
+ def get_payments_config(cls) -> PaymentsSettings:
25
+ """Get payments configuration from django-cfg."""
26
+ if cls._payments_config_cache is None:
27
+ cls._payments_config_cache = cls._load_payments_config()
28
+ return cls._payments_config_cache
29
+
30
+ @classmethod
31
+ def _load_payments_config(cls) -> PaymentsSettings:
32
+ """Load payments configuration using BaseCfgModule."""
33
+ try:
34
+ if cls._config_module is None:
35
+ from ..config.module import PaymentsCfgModule
36
+ cls._config_module = PaymentsCfgModule()
37
+
38
+ return cls._config_module.get_config()
39
+ except Exception as e:
40
+ logger.warning(f"Failed to load payments config: {e}")
41
+ return PaymentsSettings()
42
+
43
+ @classmethod
44
+ def reset_config_cache(cls):
45
+ """Reset configuration cache."""
46
+ cls._payments_config_cache = None
47
+ if cls._config_module:
48
+ cls._config_module.reset_cache()
49
+
50
+
51
+ class RedisConfigHelper(PaymentsConfigMixin):
52
+ """Helper for Redis configuration."""
53
+
54
+ @classmethod
55
+ def get_redis_config(cls) -> Dict[str, Any]:
56
+ """Get Redis configuration for payments."""
57
+ config = cls.get_payments_config()
58
+
59
+ # Default Redis settings
60
+ redis_config = {
61
+ 'host': 'localhost',
62
+ 'port': 6379,
63
+ 'db': 0,
64
+ 'decode_responses': True,
65
+ 'socket_timeout': 5,
66
+ 'socket_connect_timeout': 5,
67
+ 'retry_on_timeout': True,
68
+ 'health_check_interval': 30,
69
+ }
70
+
71
+ # Try to get Redis settings from Django CACHES
72
+ django_cache = getattr(settings, 'CACHES', {}).get('default', {})
73
+ if 'redis' in django_cache.get('BACKEND', '').lower():
74
+ location = django_cache.get('LOCATION', '')
75
+ if location.startswith('redis://'):
76
+ # Parse redis://host:port/db format
77
+ try:
78
+ # Simple parsing for redis://host:port/db
79
+ parts = location.replace('redis://', '').split('/')
80
+ host_port = parts[0].split(':')
81
+ redis_config['host'] = host_port[0]
82
+ if len(host_port) > 1:
83
+ redis_config['port'] = int(host_port[1])
84
+ if len(parts) > 1:
85
+ redis_config['db'] = int(parts[1])
86
+ except (ValueError, IndexError) as e:
87
+ logger.warning(f"Failed to parse Redis URL {location}: {e}")
88
+
89
+ # Override with payments-specific Redis config if available
90
+ if hasattr(config, 'redis') and config.redis:
91
+ redis_config.update(config.redis.dict())
92
+
93
+ return redis_config
94
+
95
+ @classmethod
96
+ def is_redis_available(cls) -> bool:
97
+ """Check if Redis is available and configured."""
98
+ try:
99
+ import redis
100
+ config = cls.get_redis_config()
101
+ client = redis.Redis(**config)
102
+ client.ping()
103
+ return True
104
+ except Exception as e:
105
+ logger.debug(f"Redis not available: {e}")
106
+ return False
107
+
108
+
109
+ class CacheConfigHelper(PaymentsConfigMixin):
110
+ """Helper for cache configuration."""
111
+
112
+ @classmethod
113
+ def get_cache_backend_type(cls) -> str:
114
+ """Get Django cache backend type."""
115
+ django_cache = getattr(settings, 'CACHES', {}).get('default', {})
116
+ backend = django_cache.get('BACKEND', '').lower()
117
+
118
+ if 'redis' in backend:
119
+ return 'redis'
120
+ elif 'memcached' in backend:
121
+ return 'memcached'
122
+ elif 'database' in backend:
123
+ return 'database'
124
+ elif 'dummy' in backend:
125
+ return 'dummy'
126
+ else:
127
+ return 'unknown'
128
+
129
+ @classmethod
130
+ def is_cache_enabled(cls) -> bool:
131
+ """Check if cache is properly configured (not dummy)."""
132
+ return cls.get_cache_backend_type() != 'dummy'
133
+
134
+ @classmethod
135
+ def get_cache_timeout(cls, operation: str) -> int:
136
+ """Get cache timeout for specific operation."""
137
+ config = cls.get_payments_config()
138
+
139
+ timeouts = {
140
+ 'api_key': 300, # 5 minutes
141
+ 'rate_limit': 3600, # 1 hour
142
+ 'session': 1800, # 30 minutes
143
+ 'default': 600 # 10 minutes
144
+ }
145
+
146
+ # Override with config if available
147
+ if hasattr(config, 'cache_timeouts') and config.cache_timeouts:
148
+ timeouts.update(config.cache_timeouts)
149
+
150
+ return timeouts.get(operation, timeouts['default'])
151
+
152
+
153
+ class ProviderConfigHelper(PaymentsConfigMixin):
154
+ """Helper for payment provider configuration."""
155
+
156
+ @classmethod
157
+ def get_enabled_providers(cls) -> list:
158
+ """Get list of enabled payment providers."""
159
+ config = cls.get_payments_config()
160
+ if not config.enabled:
161
+ return []
162
+
163
+ enabled = []
164
+ if hasattr(config, 'providers') and config.providers:
165
+ for provider_name, provider_config in config.providers.items():
166
+ if provider_config and cls._is_provider_properly_configured(provider_name, provider_config):
167
+ enabled.append(provider_name)
168
+
169
+ return enabled
170
+
171
+ @classmethod
172
+ def get_provider_config(cls, provider_name: str) -> Optional[Any]:
173
+ """Get configuration for specific provider."""
174
+ config = cls.get_payments_config()
175
+ if not config.enabled or not hasattr(config, 'providers'):
176
+ return None
177
+
178
+ return config.providers.get(provider_name)
179
+
180
+ @classmethod
181
+ def is_provider_enabled(cls, provider_name: str) -> bool:
182
+ """Check if specific provider is enabled and configured."""
183
+ return provider_name in cls.get_enabled_providers()
184
+
185
+ @classmethod
186
+ def _is_provider_properly_configured(cls, provider_name: str, provider_config: Any) -> bool:
187
+ """Check if provider configuration is complete."""
188
+ if not provider_config:
189
+ return False
190
+
191
+ # Basic validation - each provider should have api_key
192
+ if not hasattr(provider_config, 'api_key') or not provider_config.api_key:
193
+ return False
194
+
195
+ # Provider-specific validations
196
+ if provider_name == 'nowpayments':
197
+ return True # api_key is sufficient
198
+ elif provider_name == 'stripe':
199
+ return True # api_key is sufficient
200
+ elif provider_name == 'cryptapi':
201
+ return hasattr(provider_config, 'own_address') and provider_config.own_address
202
+
203
+ return True
204
+
205
+
206
+ class PaymentsConfigUtil:
207
+ """
208
+ Universal utility for payments configuration.
209
+
210
+ Combines all config helpers into one convenient interface.
211
+ """
212
+
213
+ redis = RedisConfigHelper
214
+ cache = CacheConfigHelper
215
+ providers = ProviderConfigHelper
216
+
217
+ @staticmethod
218
+ def get_config() -> PaymentsSettings:
219
+ """Get payments configuration."""
220
+ return PaymentsConfigMixin.get_payments_config()
221
+
222
+ @staticmethod
223
+ def is_payments_enabled() -> bool:
224
+ """Check if payments module is enabled."""
225
+ config = PaymentsConfigMixin.get_payments_config()
226
+ return config.enabled
227
+
228
+ @staticmethod
229
+ def is_debug_mode() -> bool:
230
+ """Check if payments module is in debug mode."""
231
+ config = PaymentsConfigMixin.get_payments_config()
232
+ return getattr(config, 'debug_mode', False)
233
+
234
+ @staticmethod
235
+ def reset_all_caches():
236
+ """Reset all configuration caches."""
237
+ PaymentsConfigMixin.reset_config_cache()
238
+
239
+
240
+ # Convenience exports
241
+ get_payments_config = PaymentsConfigUtil.get_config
242
+ is_payments_enabled = PaymentsConfigUtil.is_payments_enabled
243
+ is_debug_mode = PaymentsConfigUtil.is_debug_mode