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,198 @@
1
+ """
2
+ Configuration utility functions.
3
+
4
+ Helper functions for working with payment configurations.
5
+ """
6
+
7
+ from typing import Optional, List, Dict, Any
8
+ import logging
9
+
10
+ from .module import PaymentsCfgModule
11
+ from .settings import PaymentsSettings
12
+ from .providers import PaymentProviderConfig, NowPaymentsConfig, StripeConfig, CryptAPIConfig
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Global payments configuration instance
17
+ _payments_config = PaymentsCfgModule()
18
+
19
+
20
+ def get_payments_config() -> PaymentsSettings:
21
+ """Get current payments configuration."""
22
+ return _payments_config.get_config()
23
+
24
+
25
+ def get_provider_config(provider_name: str) -> Optional[PaymentProviderConfig]:
26
+ """Get configuration for specific payment provider."""
27
+ config = get_payments_config()
28
+ return config.providers.get(provider_name)
29
+
30
+
31
+ def get_nowpayments_config() -> Optional[NowPaymentsConfig]:
32
+ """Get NowPayments configuration."""
33
+ provider_config = get_provider_config('nowpayments')
34
+ if isinstance(provider_config, NowPaymentsConfig):
35
+ return provider_config
36
+ return None
37
+
38
+
39
+ def get_stripe_config() -> Optional[StripeConfig]:
40
+ """Get Stripe configuration."""
41
+ provider_config = get_provider_config('stripe')
42
+ if isinstance(provider_config, StripeConfig):
43
+ return provider_config
44
+ return None
45
+
46
+
47
+ def get_cryptapi_config() -> Optional[CryptAPIConfig]:
48
+ """Get CryptAPI configuration."""
49
+ provider_config = get_provider_config('cryptapi')
50
+ if isinstance(provider_config, CryptAPIConfig):
51
+ return provider_config
52
+ return None
53
+
54
+
55
+
56
+
57
+ def is_payments_enabled() -> bool:
58
+ """Check if payments module is enabled."""
59
+ try:
60
+ config = get_payments_config()
61
+ return config.enabled
62
+ except Exception as e:
63
+ logger.warning(f"Error checking payments status: {e}")
64
+ return False
65
+
66
+
67
+ def is_feature_enabled(feature_name: str) -> bool:
68
+ """Check if specific payment feature is enabled."""
69
+ try:
70
+ config = get_payments_config()
71
+ feature_map = {
72
+ 'crypto_payments': config.enable_crypto_payments,
73
+ 'fiat_payments': config.enable_fiat_payments,
74
+ 'subscriptions': config.enable_subscription_system,
75
+ 'balance': config.enable_balance_system,
76
+ 'api_keys': config.enable_api_key_system,
77
+ 'webhooks': config.enable_webhook_processing,
78
+ }
79
+ return feature_map.get(feature_name, False)
80
+ except Exception as e:
81
+ logger.warning(f"Error checking feature {feature_name}: {e}")
82
+ return False
83
+
84
+
85
+ def get_enabled_providers() -> List[str]:
86
+ """Get list of enabled payment providers."""
87
+ try:
88
+ config = get_payments_config()
89
+ return [name for name, provider_config in config.providers.items() if provider_config.enabled]
90
+ except Exception as e:
91
+ logger.warning(f"Error getting enabled providers: {e}")
92
+ return []
93
+
94
+
95
+ def get_provider_settings(provider_name: str) -> Dict[str, Any]:
96
+ """Get provider settings as dictionary for service initialization."""
97
+ try:
98
+ provider_config = get_provider_config(provider_name)
99
+ if provider_config:
100
+ return provider_config.get_config_dict()
101
+ return {}
102
+ except Exception as e:
103
+ logger.error(f"Error getting settings for provider {provider_name}: {e}")
104
+ return {}
105
+
106
+
107
+ def validate_provider_config(provider_name: str) -> bool:
108
+ """Validate provider configuration."""
109
+ try:
110
+ provider_config = get_provider_config(provider_name)
111
+ if not provider_config:
112
+ return False
113
+
114
+ # Basic validation - check if API key exists
115
+ config_dict = provider_config.get_config_dict()
116
+ return bool(config_dict.get('api_key'))
117
+
118
+ except Exception as e:
119
+ logger.error(f"Error validating provider {provider_name}: {e}")
120
+ return False
121
+
122
+
123
+ def get_rate_limit_settings() -> Dict[str, int]:
124
+ """Get rate limiting settings."""
125
+ try:
126
+ config = get_payments_config()
127
+ return {
128
+ 'hourly_limit': config.rate_limits.default_rate_limit_per_hour,
129
+ 'daily_limit': config.rate_limits.default_rate_limit_per_day,
130
+ 'burst_multiplier': config.rate_limits.burst_limit_multiplier,
131
+ 'window_size': config.rate_limits.sliding_window_size,
132
+ }
133
+ except Exception as e:
134
+ logger.warning(f"Error getting rate limit settings: {e}")
135
+ return {
136
+ 'hourly_limit': 1000,
137
+ 'daily_limit': 10000,
138
+ 'burst_multiplier': 2.0,
139
+ 'window_size': 3600,
140
+ }
141
+
142
+
143
+ def get_cache_settings() -> Dict[str, int]:
144
+ """Get cache timeout settings."""
145
+ try:
146
+ config = get_payments_config()
147
+ return {
148
+ 'access_check': config.cache.cache_timeout_access_check,
149
+ 'user_balance': config.cache.cache_timeout_user_balance,
150
+ 'subscriptions': config.cache.cache_timeout_subscriptions,
151
+ 'provider_status': config.cache.cache_timeout_provider_status,
152
+ 'currency_rates': config.cache.cache_timeout_currency_rates,
153
+ }
154
+ except Exception as e:
155
+ logger.warning(f"Error getting cache settings: {e}")
156
+ return {
157
+ 'access_check': 60,
158
+ 'user_balance': 300,
159
+ 'subscriptions': 600,
160
+ 'provider_status': 1800,
161
+ 'currency_rates': 3600,
162
+ }
163
+
164
+
165
+ def get_billing_settings() -> Dict[str, Any]:
166
+ """Get billing settings."""
167
+ try:
168
+ config = get_payments_config()
169
+ return {
170
+ 'auto_bill': config.billing.auto_bill_subscriptions,
171
+ 'grace_period_hours': config.billing.billing_grace_period_hours,
172
+ 'retry_failed': config.billing.retry_failed_payments,
173
+ 'max_retries': config.billing.max_payment_retries,
174
+ 'min_amount_usd': config.billing.min_payment_amount_usd,
175
+ 'max_amount_usd': config.billing.max_payment_amount_usd,
176
+ }
177
+ except Exception as e:
178
+ logger.warning(f"Error getting billing settings: {e}")
179
+ return {
180
+ 'auto_bill': True,
181
+ 'grace_period_hours': 24,
182
+ 'retry_failed': True,
183
+ 'max_retries': 3,
184
+ 'min_amount_usd': 1.0,
185
+ 'max_amount_usd': 50000.0,
186
+ }
187
+
188
+
189
+ def reset_config_cache():
190
+ """Reset configuration cache (useful for testing)."""
191
+ global _payments_config
192
+ _payments_config.reset_cache()
193
+
194
+
195
+ def reload_config():
196
+ """Reload configuration from project settings."""
197
+ reset_config_cache()
198
+ return get_payments_config()
@@ -0,0 +1,291 @@
1
+ """
2
+ Decorators for API access control and endpoint registration.
3
+ """
4
+
5
+ import functools
6
+ import logging
7
+ from typing import Optional, List, Callable, Any
8
+ from django.http import JsonResponse
9
+ from django.conf import settings
10
+ from django.utils import timezone
11
+ from .models import EndpointGroup, Subscription
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def require_api_key(func: Callable) -> Callable:
17
+ """
18
+ Decorator to require valid API key for function-based views.
19
+ Works with APIAccessMiddleware.
20
+ """
21
+ @functools.wraps(func)
22
+ def wrapper(request, *args, **kwargs):
23
+ if not hasattr(request, 'payment_api_key'):
24
+ return JsonResponse({
25
+ 'error': {
26
+ 'code': 'MISSING_API_KEY',
27
+ 'message': 'Valid API key required',
28
+ 'timestamp': timezone.now().isoformat(),
29
+ }
30
+ }, status=401)
31
+
32
+ return func(request, *args, **kwargs)
33
+
34
+ return wrapper
35
+
36
+
37
+ def require_subscription(endpoint_group_name: str):
38
+ """
39
+ Decorator to require active subscription for specific endpoint group.
40
+
41
+ Args:
42
+ endpoint_group_name: Name of the endpoint group to check
43
+ """
44
+ def decorator(func: Callable) -> Callable:
45
+ @functools.wraps(func)
46
+ def wrapper(request, *args, **kwargs):
47
+ # Check if middleware already validated subscription
48
+ if hasattr(request, 'payment_subscription'):
49
+ subscription = request.payment_subscription
50
+ if subscription.endpoint_group.name == endpoint_group_name:
51
+ return func(request, *args, **kwargs)
52
+
53
+ # If not validated by middleware, check manually
54
+ if not hasattr(request, 'payment_api_key'):
55
+ return JsonResponse({
56
+ 'error': {
57
+ 'code': 'MISSING_API_KEY',
58
+ 'message': 'Valid API key required',
59
+ 'timestamp': timezone.now().isoformat(),
60
+ }
61
+ }, status=401)
62
+
63
+ try:
64
+ endpoint_group = EndpointGroup.objects.get(
65
+ name=endpoint_group_name,
66
+ is_active=True
67
+ )
68
+
69
+ subscription = Subscription.objects.filter(
70
+ user=request.payment_api_key.user,
71
+ endpoint_group=endpoint_group,
72
+ status='active',
73
+ expires_at__gt=timezone.now()
74
+ ).first()
75
+
76
+ if not subscription:
77
+ return JsonResponse({
78
+ 'error': {
79
+ 'code': 'NO_SUBSCRIPTION',
80
+ 'message': f'Active subscription required for {endpoint_group.display_name}',
81
+ 'timestamp': timezone.now().isoformat(),
82
+ }
83
+ }, status=403)
84
+
85
+ # Store subscription in request
86
+ request.payment_subscription = subscription
87
+
88
+ return func(request, *args, **kwargs)
89
+
90
+ except EndpointGroup.DoesNotExist:
91
+ logger.error(f"Endpoint group '{endpoint_group_name}' not found")
92
+ return JsonResponse({
93
+ 'error': {
94
+ 'code': 'INVALID_ENDPOINT_GROUP',
95
+ 'message': 'Invalid endpoint group',
96
+ 'timestamp': timezone.now().isoformat(),
97
+ }
98
+ }, status=500)
99
+
100
+ return wrapper
101
+ return decorator
102
+
103
+
104
+ def require_tier(minimum_tier: str):
105
+ """
106
+ Decorator to require minimum subscription tier.
107
+
108
+ Args:
109
+ minimum_tier: Minimum required tier (basic, premium, enterprise)
110
+ """
111
+ tier_hierarchy = {
112
+ 'basic': 1,
113
+ 'premium': 2,
114
+ 'enterprise': 3,
115
+ }
116
+
117
+ def decorator(func: Callable) -> Callable:
118
+ @functools.wraps(func)
119
+ def wrapper(request, *args, **kwargs):
120
+ if not hasattr(request, 'payment_subscription'):
121
+ return JsonResponse({
122
+ 'error': {
123
+ 'code': 'NO_SUBSCRIPTION',
124
+ 'message': 'Active subscription required',
125
+ 'timestamp': timezone.now().isoformat(),
126
+ }
127
+ }, status=403)
128
+
129
+ subscription = request.payment_subscription
130
+ current_tier_level = tier_hierarchy.get(subscription.tier, 0)
131
+ required_tier_level = tier_hierarchy.get(minimum_tier, 999)
132
+
133
+ if current_tier_level < required_tier_level:
134
+ return JsonResponse({
135
+ 'error': {
136
+ 'code': 'INSUFFICIENT_TIER',
137
+ 'message': f'Tier {minimum_tier} or higher required',
138
+ 'current_tier': subscription.tier,
139
+ 'required_tier': minimum_tier,
140
+ 'timestamp': timezone.now().isoformat(),
141
+ }
142
+ }, status=403)
143
+
144
+ return func(request, *args, **kwargs)
145
+
146
+ return wrapper
147
+ return decorator
148
+
149
+
150
+ def track_usage(cost_per_request: float = 0.0):
151
+ """
152
+ Decorator to track API usage and deduct costs.
153
+
154
+ Args:
155
+ cost_per_request: Cost to deduct per successful request
156
+ """
157
+ def decorator(func: Callable) -> Callable:
158
+ @functools.wraps(func)
159
+ def wrapper(request, *args, **kwargs):
160
+ # Execute the function
161
+ response = func(request, *args, **kwargs)
162
+
163
+ # Track usage if successful and we have subscription
164
+ if (hasattr(request, 'payment_subscription') and
165
+ hasattr(response, 'status_code') and
166
+ 200 <= response.status_code < 300 and
167
+ cost_per_request > 0):
168
+
169
+ try:
170
+ from .models import Transaction
171
+
172
+ subscription = request.payment_subscription
173
+
174
+ # Create billing transaction
175
+ Transaction.objects.create(
176
+ user=subscription.user,
177
+ subscription=subscription,
178
+ transaction_type='debit',
179
+ amount_usd=-cost_per_request,
180
+ description=f"API usage: {request.method} {request.path}",
181
+ metadata={
182
+ 'endpoint': request.path,
183
+ 'method': request.method,
184
+ 'cost_per_request': cost_per_request,
185
+ }
186
+ )
187
+
188
+ except Exception as e:
189
+ logger.error(f"Error tracking usage: {e}")
190
+
191
+ return response
192
+
193
+ return wrapper
194
+ return decorator
195
+
196
+
197
+ def register_endpoint(endpoint_group_name: str,
198
+ display_name: Optional[str] = None,
199
+ description: Optional[str] = None,
200
+ require_api_key: bool = True):
201
+ """
202
+ Decorator to automatically register endpoint with the system.
203
+ This creates or updates EndpointGroup records.
204
+
205
+ Args:
206
+ endpoint_group_name: Internal name for the endpoint group
207
+ display_name: Human-readable name
208
+ description: Description of the endpoint functionality
209
+ require_api_key: Whether this endpoint requires API key
210
+ """
211
+ def decorator(func: Callable) -> Callable:
212
+ @functools.wraps(func)
213
+ def wrapper(*args, **kwargs):
214
+ # Auto-register endpoint group if it doesn't exist
215
+ try:
216
+ endpoint_group, created = EndpointGroup.objects.get_or_create(
217
+ name=endpoint_group_name,
218
+ defaults={
219
+ 'display_name': display_name or endpoint_group_name.replace('_', ' ').title(),
220
+ 'description': description or f'Auto-registered endpoint group: {endpoint_group_name}',
221
+ 'require_api_key': require_api_key,
222
+ 'is_active': True,
223
+ }
224
+ )
225
+
226
+ if created:
227
+ logger.info(f"Auto-registered endpoint group: {endpoint_group_name}")
228
+
229
+ except Exception as e:
230
+ logger.error(f"Error auto-registering endpoint group: {e}")
231
+
232
+ return func(*args, **kwargs)
233
+
234
+ return wrapper
235
+ return decorator
236
+
237
+
238
+ def check_usage_limit(func: Callable) -> Callable:
239
+ """
240
+ Decorator to check subscription usage limits before processing request.
241
+ """
242
+ @functools.wraps(func)
243
+ def wrapper(request, *args, **kwargs):
244
+ if hasattr(request, 'payment_subscription'):
245
+ subscription = request.payment_subscription
246
+
247
+ # Check if usage limit exceeded
248
+ if (subscription.usage_limit > 0 and
249
+ subscription.usage_current >= subscription.usage_limit):
250
+
251
+ return JsonResponse({
252
+ 'error': {
253
+ 'code': 'USAGE_EXCEEDED',
254
+ 'message': 'Monthly usage limit exceeded',
255
+ 'current_usage': subscription.usage_current,
256
+ 'usage_limit': subscription.usage_limit,
257
+ 'reset_date': subscription.next_billing.isoformat() if subscription.next_billing else None,
258
+ 'timestamp': timezone.now().isoformat(),
259
+ }
260
+ }, status=429)
261
+
262
+ return func(request, *args, **kwargs)
263
+
264
+ return wrapper
265
+
266
+
267
+ # Utility decorator combinations
268
+ def api_endpoint(endpoint_group_name: str,
269
+ minimum_tier: str = 'basic',
270
+ cost_per_request: float = 0.0):
271
+ """
272
+ Combination decorator for typical API endpoint protection.
273
+
274
+ Args:
275
+ endpoint_group_name: Name of the endpoint group
276
+ minimum_tier: Minimum subscription tier required
277
+ cost_per_request: Cost to charge per successful request
278
+ """
279
+ def decorator(func: Callable) -> Callable:
280
+ # Apply decorators in reverse order (they wrap from inside out)
281
+ decorated_func = func
282
+ decorated_func = track_usage(cost_per_request)(decorated_func)
283
+ decorated_func = check_usage_limit(decorated_func)
284
+ decorated_func = require_tier(minimum_tier)(decorated_func)
285
+ decorated_func = require_subscription(endpoint_group_name)(decorated_func)
286
+ decorated_func = require_api_key(decorated_func)
287
+ decorated_func = register_endpoint(endpoint_group_name)(decorated_func)
288
+
289
+ return decorated_func
290
+
291
+ return decorator